Compare commits
35 Commits
backup-est
...
migration/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6070099d1 | ||
|
|
8a49b19d00 | ||
|
|
9d14f38965 | ||
|
|
f35b60ed4e | ||
|
|
7cc5f194e9 | ||
|
|
6dc052afa6 | ||
|
|
8878afe168 | ||
|
|
7a8daa72c6 | ||
|
|
f52a395e0d | ||
|
|
6e75527157 | ||
|
|
4f11c2c312 | ||
|
|
1a4d9d8c08 | ||
|
|
71cfd54166 | ||
|
|
4c807e1cf2 | ||
|
|
0846a3bf03 | ||
|
|
90de6df77c | ||
|
|
677fbd4368 | ||
|
|
42edfab50d | ||
|
|
e34fd28df7 | ||
|
|
de5fff4f5c | ||
|
|
b782ebceee | ||
|
|
a6578f4973 | ||
|
|
77dd809e8c | ||
|
|
60b3992ca5 | ||
|
|
49b923230f | ||
|
|
9e29410c0d | ||
|
|
f0989f4fb0 | ||
|
|
3947e36c98 | ||
|
|
03c97d31d3 | ||
|
|
e94b274ed0 | ||
|
|
883853bc5c | ||
|
|
1c6b184e94 | ||
|
|
8a99f184bf | ||
|
|
3ad2413e7a | ||
|
|
4818d90386 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -42,6 +42,11 @@ npm-debug.log
|
||||
|
||||
# Composer (si hay dependencias PHP)
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# PHPUnit
|
||||
.phpunit.result.cache
|
||||
/tests/_output/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
@@ -65,7 +70,11 @@ vendor/
|
||||
# Planning and documentation
|
||||
_planeacion/
|
||||
|
||||
# Testing infrastructure (composer, phpunit, phpcs configs and dependencies)
|
||||
_testing-suite/
|
||||
|
||||
# Claude Code tools
|
||||
.playwright-mcp/
|
||||
.serena/
|
||||
.claude/
|
||||
nul
|
||||
|
||||
18
404.php
18
404.php
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#404-not-found
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,7 @@ get_header();
|
||||
<!-- Error Header -->
|
||||
<header class="page-header">
|
||||
<h1 id="error-404-title" class="page-title">
|
||||
<?php esc_html_e( '404 - Page Not Found', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( '404 - Page Not Found', 'roi-theme' ); ?>
|
||||
</h1>
|
||||
</header><!-- .page-header -->
|
||||
|
||||
@@ -31,25 +31,25 @@ get_header();
|
||||
<div class="page-content">
|
||||
|
||||
<p class="error-message">
|
||||
<?php esc_html_e( 'Oops! The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Oops! The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.', 'roi-theme' ); ?>
|
||||
</p>
|
||||
|
||||
<!-- Helpful Actions -->
|
||||
<div class="error-actions">
|
||||
|
||||
<h2><?php esc_html_e( 'What can you do?', 'apus-theme' ); ?></h2>
|
||||
<h2><?php esc_html_e( 'What can you do?', 'roi-theme' ); ?></h2>
|
||||
|
||||
<ul class="error-suggestions" role="list">
|
||||
<li>
|
||||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
|
||||
<?php esc_html_e( 'Go to the homepage', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Go to the homepage', 'roi-theme' ); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'Check the URL for typos', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Check the URL for typos', 'roi-theme' ); ?>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'Use the navigation menu above', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Use the navigation menu above', 'roi-theme' ); ?>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -65,7 +65,7 @@ get_header();
|
||||
if ( ! empty( $recent_posts ) ) :
|
||||
?>
|
||||
<div class="recent-posts-section">
|
||||
<h3><?php esc_html_e( 'Recent Posts', 'apus-theme' ); ?></h3>
|
||||
<h3><?php esc_html_e( 'Recent Posts', 'roi-theme' ); ?></h3>
|
||||
<ul class="recent-posts-list" role="list">
|
||||
<?php foreach ( $recent_posts as $recent ) : ?>
|
||||
<li>
|
||||
@@ -95,7 +95,7 @@ get_header();
|
||||
if ( ! empty( $categories ) ) :
|
||||
?>
|
||||
<div class="categories-section">
|
||||
<h3><?php esc_html_e( 'Browse by Category', 'apus-theme' ); ?></h3>
|
||||
<h3><?php esc_html_e( 'Browse by Category', 'roi-theme' ); ?></h3>
|
||||
<ul class="categories-list" role="list">
|
||||
<?php foreach ( $categories as $category ) : ?>
|
||||
<li>
|
||||
|
||||
0
Admin/.gitkeep
Normal file
0
Admin/.gitkeep
Normal file
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Contact Form
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class ContactFormFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'contact-form';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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,76 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Box Sidebar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* UBICACION:
|
||||
* - Dentro del modulo CtaBoxSidebar (autocontenido)
|
||||
* - Eliminar modulo = eliminar mapper
|
||||
*/
|
||||
final class CtaBoxSidebarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-box-sidebar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'ctaDescription' => ['group' => 'content', 'attribute' => 'description'],
|
||||
'ctaButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaButtonIcon' => ['group' => 'content', 'attribute' => 'button_icon'],
|
||||
'ctaButtonAction' => ['group' => 'content', 'attribute' => 'button_action'],
|
||||
'ctaButtonLink' => ['group' => 'content', 'attribute' => 'button_link'],
|
||||
|
||||
// Behavior
|
||||
'ctaTextAlign' => ['group' => 'behavior', 'attribute' => 'text_align'],
|
||||
|
||||
// Typography
|
||||
'ctaTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'ctaTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'ctaDescFontSize' => ['group' => 'typography', 'attribute' => 'description_font_size'],
|
||||
'ctaButtonFontSize' => ['group' => 'typography', 'attribute' => 'button_font_size'],
|
||||
'ctaButtonFontWeight' => ['group' => 'typography', 'attribute' => 'button_font_weight'],
|
||||
|
||||
// Colors
|
||||
'ctaBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'ctaTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'ctaDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'ctaButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_background_color'],
|
||||
'ctaButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'ctaButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_background'],
|
||||
'ctaButtonHoverText' => ['group' => 'colors', 'attribute' => 'button_hover_text_color'],
|
||||
|
||||
// Spacing
|
||||
'ctaContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'ctaTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'ctaDescMarginBottom' => ['group' => 'spacing', 'attribute' => 'description_margin_bottom'],
|
||||
'ctaButtonPadding' => ['group' => 'spacing', 'attribute' => 'button_padding'],
|
||||
'ctaIconMarginRight' => ['group' => 'spacing', 'attribute' => 'icon_margin_right'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'ctaBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'ctaTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Lets Talk
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class CtaLetsTalkFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-lets-talk';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaPost\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Post
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class CtaPostFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-post';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#212529');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonTextColor', 'Texto', $buttonTextColor);
|
||||
|
||||
$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,48 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Featured Image
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class FeaturedImageFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'featured-image';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Footer\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Footer
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class FooterFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'footer';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'footerEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'footerShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'footerShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Widget 1
|
||||
'footerWidget1Visible' => ['group' => 'widget_1', 'attribute' => 'widget_1_visible'],
|
||||
'footerWidget1Title' => ['group' => 'widget_1', 'attribute' => 'widget_1_title'],
|
||||
|
||||
// Widget 2
|
||||
'footerWidget2Visible' => ['group' => 'widget_2', 'attribute' => 'widget_2_visible'],
|
||||
'footerWidget2Title' => ['group' => 'widget_2', 'attribute' => 'widget_2_title'],
|
||||
|
||||
// Widget 3
|
||||
'footerWidget3Visible' => ['group' => 'widget_3', 'attribute' => 'widget_3_visible'],
|
||||
'footerWidget3Title' => ['group' => 'widget_3', 'attribute' => 'widget_3_title'],
|
||||
|
||||
// Newsletter
|
||||
'footerNewsletterVisible' => ['group' => 'newsletter', 'attribute' => 'newsletter_visible'],
|
||||
'footerNewsletterTitle' => ['group' => 'newsletter', 'attribute' => 'newsletter_title'],
|
||||
'footerNewsletterDescription' => ['group' => 'newsletter', 'attribute' => 'newsletter_description'],
|
||||
'footerNewsletterPlaceholder' => ['group' => 'newsletter', 'attribute' => 'newsletter_email_placeholder'],
|
||||
'footerNewsletterButtonText' => ['group' => 'newsletter', 'attribute' => 'newsletter_button_text'],
|
||||
'footerNewsletterWebhookUrl' => ['group' => 'newsletter', 'attribute' => 'newsletter_webhook_url'],
|
||||
'footerNewsletterSuccessMessage' => ['group' => 'newsletter', 'attribute' => 'newsletter_success_message'],
|
||||
'footerNewsletterErrorMessage' => ['group' => 'newsletter', 'attribute' => 'newsletter_error_message'],
|
||||
|
||||
// Footer Bottom
|
||||
'footerCopyrightText' => ['group' => 'footer_bottom', 'attribute' => 'copyright_text'],
|
||||
|
||||
// Colors
|
||||
'footerBgColor' => ['group' => 'colors', 'attribute' => 'bg_color'],
|
||||
'footerTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'footerTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'footerLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'footerLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
'footerButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'footerButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'footerButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
|
||||
// Spacing
|
||||
'footerPaddingY' => ['group' => 'spacing', 'attribute' => 'padding_y'],
|
||||
'footerMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||
|
||||
// Visual Effects
|
||||
'footerInputBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'input_border_radius'],
|
||||
'footerButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'footerTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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_email_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->buildTextarea('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;
|
||||
}
|
||||
}
|
||||
65
Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
Normal file
65
Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Hero\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Hero Section
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class HeroFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'hero';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
165
Admin/Infrastructure/Ui/AdminDashboardRenderer.php
Normal file
165
Admin/Infrastructure/Ui/AdminDashboardRenderer.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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',
|
||||
],
|
||||
'theme-settings' => [
|
||||
'id' => 'theme-settings',
|
||||
'label' => 'Theme Settings',
|
||||
'icon' => 'bi-gear',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.3rem;
|
||||
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;
|
||||
}
|
||||
}
|
||||
460
Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
Normal file
460
Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
Normal file
@@ -0,0 +1,460 @@
|
||||
/**
|
||||
* 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 con persistencia en URL
|
||||
*/
|
||||
function initializeTabs() {
|
||||
const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]');
|
||||
|
||||
// Leer parametro admin-tab de la URL al cargar
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const activeTabParam = urlParams.get('admin-tab');
|
||||
|
||||
if (activeTabParam) {
|
||||
// Buscar el boton del tab correspondiente
|
||||
const targetButton = document.querySelector('[data-bs-target="#' + activeTabParam + 'Tab"]');
|
||||
if (targetButton) {
|
||||
// Activar el tab usando Bootstrap API
|
||||
const tab = new bootstrap.Tab(targetButton);
|
||||
tab.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Escuchar cambios de tab para actualizar URL
|
||||
tabButtons.forEach(function(tabButton) {
|
||||
tabButton.addEventListener('shown.bs.tab', function(e) {
|
||||
// Obtener el ID del componente desde data-bs-target
|
||||
const target = e.target.getAttribute('data-bs-target');
|
||||
const componentId = target.replace('#', '').replace('Tab', '');
|
||||
|
||||
// Actualizar URL sin recargar pagina
|
||||
updateUrlWithTab(componentId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza la URL con el parametro admin-tab sin recargar la pagina
|
||||
*
|
||||
* @param {string} tabId ID del tab activo
|
||||
*/
|
||||
function updateUrlWithTab(tabId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('admin-tab', tabId);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el ID del tab activo actualmente
|
||||
*
|
||||
* @returns {string|null} ID del componente activo o null
|
||||
*/
|
||||
function getActiveTabId() {
|
||||
const activeTab = document.querySelector('.tab-pane.active');
|
||||
if (activeTab) {
|
||||
return activeTab.id.replace('Tab', '');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
// Recargar preservando el tab activo
|
||||
setTimeout(() => {
|
||||
const activeTabId = getActiveTabId();
|
||||
if (activeTabId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('admin-tab', activeTabId);
|
||||
window.location.href = url.toString();
|
||||
} else {
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
88
Admin/Infrastructure/Ui/Views/dashboard.php
Normal file
88
Admin/Infrastructure/Ui/Views/dashboard.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?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();
|
||||
|
||||
// Determinar tab activo: desde URL o primer componente
|
||||
$activeComponentId = array_key_first($components);
|
||||
|
||||
// Leer parametro admin-tab de la URL con sanitizacion
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Solo lectura de parametro para UI
|
||||
if (isset($_GET['admin-tab'])) {
|
||||
$requestedTab = sanitize_text_field(wp_unslash($_GET['admin-tab']));
|
||||
// Validar que el componente exista
|
||||
if (array_key_exists($requestedTab, $components)) {
|
||||
$activeComponentId = $requestedTab;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<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 === $activeComponentId ? '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 === $activeComponentId ? '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):
|
||||
$isActive = ($componentId === $activeComponentId);
|
||||
$componentSettings = $this->getComponentSettings($componentId);
|
||||
?>
|
||||
<!-- Tab: <?php echo esc_html($component['label']); ?> -->
|
||||
<div class="tab-pane fade <?php echo $isActive ? '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 -->
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Navbar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class NavbarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'navbar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Related Post
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* NOTA: Este componente NO tenia mapeos en AdminAjaxHandler
|
||||
* (era el unico componente roto - 35 campos no se guardaban)
|
||||
*/
|
||||
final class RelatedPostFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'related-post';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'relatedPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Content
|
||||
'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
'relatedPostPerPage' => ['group' => 'content', 'attribute' => 'posts_per_page'],
|
||||
'relatedPostOrderby' => ['group' => 'content', 'attribute' => 'orderby'],
|
||||
'relatedPostOrder' => ['group' => 'content', 'attribute' => 'order'],
|
||||
'relatedPostShowPagination' => ['group' => 'content', 'attribute' => 'show_pagination'],
|
||||
|
||||
// Layout
|
||||
'relatedPostColsDesktop' => ['group' => 'layout', 'attribute' => 'columns_desktop'],
|
||||
'relatedPostColsTablet' => ['group' => 'layout', 'attribute' => 'columns_tablet'],
|
||||
'relatedPostColsMobile' => ['group' => 'layout', 'attribute' => 'columns_mobile'],
|
||||
|
||||
// Typography
|
||||
'relatedPostSectionTitleSize' => ['group' => 'typography', 'attribute' => 'section_title_size'],
|
||||
'relatedPostSectionTitleWeight' => ['group' => 'typography', 'attribute' => 'section_title_weight'],
|
||||
'relatedPostCardTitleSize' => ['group' => 'typography', 'attribute' => 'card_title_size'],
|
||||
'relatedPostCardTitleWeight' => ['group' => 'typography', 'attribute' => 'card_title_weight'],
|
||||
|
||||
// Colors
|
||||
'relatedPostSectionTitleColor' => ['group' => 'colors', 'attribute' => 'section_title_color'],
|
||||
'relatedPostCardBgColor' => ['group' => 'colors', 'attribute' => 'card_bg_color'],
|
||||
'relatedPostCardTitleColor' => ['group' => 'colors', 'attribute' => 'card_title_color'],
|
||||
'relatedPostCardHoverBgColor' => ['group' => 'colors', 'attribute' => 'card_hover_bg_color'],
|
||||
'relatedPostPaginationBgColor' => ['group' => 'colors', 'attribute' => 'pagination_bg_color'],
|
||||
'relatedPostPaginationTextColor' => ['group' => 'colors', 'attribute' => 'pagination_text_color'],
|
||||
'relatedPostPaginationActiveBg' => ['group' => 'colors', 'attribute' => 'pagination_active_bg'],
|
||||
'relatedPostPaginationActiveText' => ['group' => 'colors', 'attribute' => 'pagination_active_text'],
|
||||
|
||||
// Spacing
|
||||
'relatedPostSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||
'relatedPostSectionMarginBottom' => ['group' => 'spacing', 'attribute' => 'section_margin_bottom'],
|
||||
'relatedPostTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'relatedPostGridGap' => ['group' => 'spacing', 'attribute' => 'grid_gap'],
|
||||
'relatedPostCardPadding' => ['group' => 'spacing', 'attribute' => 'card_padding'],
|
||||
'relatedPostPaginationMarginTop' => ['group' => 'spacing', 'attribute' => 'pagination_margin_top'],
|
||||
|
||||
// Visual Effects
|
||||
'relatedPostCardBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'card_border_radius'],
|
||||
'relatedPostCardShadow' => ['group' => 'visual_effects', 'attribute' => 'card_shadow'],
|
||||
'relatedPostCardHoverShadow' => ['group' => 'visual_effects', 'attribute' => 'card_hover_shadow'],
|
||||
'relatedPostCardTransition' => ['group' => 'visual_effects', 'attribute' => 'card_transition'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
38
Admin/Shared/Domain/Contracts/FieldMapperInterface.php
Normal file
38
Admin/Shared/Domain/Contracts/FieldMapperInterface.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para mapeo de campos de formulario a atributos de BD
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Definir el mapeo de field IDs a grupos/atributos
|
||||
* - Cada modulo implementa su propio mapper
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - ISP: Interfaz pequena (2 metodos)
|
||||
* - DIP: Capas superiores dependen de esta abstraccion
|
||||
*/
|
||||
interface FieldMapperInterface
|
||||
{
|
||||
/**
|
||||
* Retorna el nombre del componente que mapea
|
||||
*
|
||||
* @return string Nombre en kebab-case (ej: 'cta-box-sidebar')
|
||||
*/
|
||||
public function getComponentName(): string;
|
||||
|
||||
/**
|
||||
* Retorna el mapeo de field IDs a grupo/atributo
|
||||
*
|
||||
* @return array<string, array{group: string, attribute: string}>
|
||||
*
|
||||
* Ejemplo:
|
||||
* [
|
||||
* 'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
* 'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
* ]
|
||||
*/
|
||||
public function getFieldMapping(): array;
|
||||
}
|
||||
145
Admin/Shared/Infrastructure/Api/Wordpress/AdminAjaxHandler.php
Normal file
145
Admin/Shared/Infrastructure/Api/Wordpress/AdminAjaxHandler.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Api\Wordpress;
|
||||
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||
use ROITheme\Admin\Shared\Infrastructure\FieldMapping\FieldMapperRegistry;
|
||||
|
||||
/**
|
||||
* Handler para peticiones AJAX del panel de administracion
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Manejar HTTP (request/response)
|
||||
* - Delegar mapeo a FieldMapperRegistry
|
||||
* - NO contiene logica de mapeo
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - SRP: Solo maneja HTTP
|
||||
* - OCP: Nuevos componentes no requieren modificar esta clase
|
||||
* - DIP: Depende de abstracciones (FieldMapperRegistry)
|
||||
*/
|
||||
final class AdminAjaxHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?SaveComponentSettingsUseCase $saveComponentSettingsUseCase = null,
|
||||
private readonly ?FieldMapperRegistry $fieldMapperRegistry = null
|
||||
) {}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_action('wp_ajax_roi_save_component_settings', [$this, 'saveComponentSettings']);
|
||||
add_action('wp_ajax_roi_reset_component_defaults', [$this, 'resetComponentDefaults']);
|
||||
}
|
||||
|
||||
public function saveComponentSettings(): void
|
||||
{
|
||||
check_ajax_referer('roi_admin_dashboard', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => 'No tienes permisos para realizar esta accion.']);
|
||||
}
|
||||
|
||||
$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.']);
|
||||
}
|
||||
|
||||
// Obtener mapper del modulo correspondiente
|
||||
if ($this->fieldMapperRegistry === null || !$this->fieldMapperRegistry->hasMapper($component)) {
|
||||
wp_send_json_error([
|
||||
'message' => "No existe mapper para el componente: {$component}"
|
||||
]);
|
||||
}
|
||||
|
||||
$mapper = $this->fieldMapperRegistry->getMapper($component);
|
||||
$fieldMapping = $mapper->getFieldMapping();
|
||||
|
||||
// Mapear settings usando el mapper del modulo
|
||||
$mappedSettings = $this->mapSettings($settings, $fieldMapping);
|
||||
|
||||
// Guardar usando Use Case
|
||||
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.']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapea settings de field IDs a grupos/atributos
|
||||
*/
|
||||
private function mapSettings(array $settings, array $fieldMapping): array
|
||||
{
|
||||
$mappedSettings = [];
|
||||
|
||||
foreach ($settings as $fieldId => $value) {
|
||||
if (!isset($fieldMapping[$fieldId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapping = $fieldMapping[$fieldId];
|
||||
$groupName = $mapping['group'];
|
||||
$attributeName = $mapping['attribute'];
|
||||
|
||||
if (!isset($mappedSettings[$groupName])) {
|
||||
$mappedSettings[$groupName] = [];
|
||||
}
|
||||
|
||||
$mappedSettings[$groupName][$attributeName] = $value;
|
||||
}
|
||||
|
||||
return $mappedSettings;
|
||||
}
|
||||
|
||||
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 accion.'
|
||||
]);
|
||||
}
|
||||
|
||||
// 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.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Provider para auto-registro de Field Mappers
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Descubrir automaticamente FieldMappers en cada modulo
|
||||
* - Registrarlos en el FieldMapperRegistry
|
||||
*
|
||||
* BENEFICIO:
|
||||
* - Agregar nuevo componente = crear FieldMapper (sin tocar functions.php)
|
||||
* - Eliminar componente = borrar carpeta (limpieza automatica)
|
||||
*/
|
||||
final class FieldMapperProvider
|
||||
{
|
||||
private const MODULES = [
|
||||
'TopNotificationBar',
|
||||
'Navbar',
|
||||
'CtaLetsTalk',
|
||||
'Hero',
|
||||
'FeaturedImage',
|
||||
'TableOfContents',
|
||||
'CtaBoxSidebar',
|
||||
'SocialShare',
|
||||
'CtaPost',
|
||||
'RelatedPost',
|
||||
'ContactForm',
|
||||
'Footer',
|
||||
'ThemeSettings',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly FieldMapperRegistry $registry
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registra todos los FieldMappers disponibles
|
||||
*/
|
||||
public function registerAll(): void
|
||||
{
|
||||
foreach (self::MODULES as $module) {
|
||||
$this->registerIfExists($module);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra un mapper si existe la clase
|
||||
*/
|
||||
private function registerIfExists(string $module): void
|
||||
{
|
||||
$className = sprintf(
|
||||
'ROITheme\\Admin\\%s\\Infrastructure\\FieldMapping\\%sFieldMapper',
|
||||
$module,
|
||||
$module
|
||||
);
|
||||
|
||||
if (class_exists($className)) {
|
||||
$mapper = new $className();
|
||||
if ($mapper instanceof FieldMapperInterface) {
|
||||
$this->registry->register($mapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Registro central de Field Mappers
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Registrar mappers de cada modulo
|
||||
* - Resolver mapper por nombre de componente
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - OCP: Nuevos mappers se registran sin modificar esta clase
|
||||
* - SRP: Solo gestiona el registro, no contiene mapeos
|
||||
*/
|
||||
final class FieldMapperRegistry
|
||||
{
|
||||
/** @var array<string, FieldMapperInterface> */
|
||||
private array $mappers = [];
|
||||
|
||||
/**
|
||||
* Registra un mapper
|
||||
*/
|
||||
public function register(FieldMapperInterface $mapper): void
|
||||
{
|
||||
$this->mappers[$mapper->getComponentName()] = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un mapper por nombre de componente
|
||||
*
|
||||
* @throws \InvalidArgumentException Si no existe mapper para el componente
|
||||
*/
|
||||
public function getMapper(string $componentName): FieldMapperInterface
|
||||
{
|
||||
if (!isset($this->mappers[$componentName])) {
|
||||
throw new \InvalidArgumentException(
|
||||
"No field mapper registered for component: {$componentName}"
|
||||
);
|
||||
}
|
||||
|
||||
return $this->mappers[$componentName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si existe mapper para un componente
|
||||
*/
|
||||
public function hasMapper(string $componentName): bool
|
||||
{
|
||||
return isset($this->mappers[$componentName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los mappers registrados
|
||||
*
|
||||
* @return array<string, FieldMapperInterface>
|
||||
*/
|
||||
public function getAllMappers(): array
|
||||
{
|
||||
return $this->mappers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\SocialShare\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Social Share
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class SocialShareFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'social-share';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
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,85 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Table of Contents
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class TableOfContentsFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'table-of-contents';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ThemeSettings\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* NOTA: Logo/branding se gestiona desde el componente navbar
|
||||
*/
|
||||
final class ThemeSettingsFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'theme-settings';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Analytics
|
||||
'themeSettingsGaTrackingId' => ['group' => 'analytics', 'attribute' => 'ga_tracking_id'],
|
||||
'themeSettingsGaAnonymizeIp' => ['group' => 'analytics', 'attribute' => 'ga_anonymize_ip'],
|
||||
|
||||
// AdSense
|
||||
'themeSettingsAdsensePublisherId' => ['group' => 'adsense', 'attribute' => 'adsense_publisher_id'],
|
||||
'themeSettingsAdsenseAutoAds' => ['group' => 'adsense', 'attribute' => 'adsense_auto_ads'],
|
||||
|
||||
// Custom Code
|
||||
'themeSettingsCustomCss' => ['group' => 'custom_code', 'attribute' => 'custom_css'],
|
||||
'themeSettingsCustomJsHeader' => ['group' => 'custom_code', 'attribute' => 'custom_js_header'],
|
||||
'themeSettingsCustomJsFooter' => ['group' => 'custom_code', 'attribute' => 'custom_js_footer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ThemeSettings\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuraciones globales del tema
|
||||
* (analytics, adsense, codigo personalizado)
|
||||
*
|
||||
* NOTA: Logo/branding se gestiona desde el componente navbar
|
||||
*
|
||||
* @package ROITheme\Admin\ThemeSettings\Infrastructure\Ui
|
||||
*/
|
||||
final class ThemeSettingsFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda - Analytics + AdSense
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildAnalyticsGroup($componentId);
|
||||
$html .= $this->buildAdSenseGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha - Custom Code
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildCustomCodeGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuraciones Globales del Tema';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Analytics, AdSense y Codigo Personalizado';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="theme-settings">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAnalyticsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-graph-up me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Analytics';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$gaTrackingId = $this->renderer->getFieldValue($componentId, 'analytics', 'ga_tracking_id', '');
|
||||
$html .= $this->buildTextInput('themeSettingsGaTrackingId', 'Google Analytics ID', 'bi-bar-chart', $gaTrackingId);
|
||||
|
||||
$html .= ' <div class="form-text small mb-2">Formato: G-XXXXXXXXXX o UA-XXXXXXXX-X</div>';
|
||||
|
||||
$gaAnonymizeIp = $this->renderer->getFieldValue($componentId, 'analytics', 'ga_anonymize_ip', true);
|
||||
$html .= $this->buildSwitch('themeSettingsGaAnonymizeIp', 'Anonimizar IP (GDPR)', 'bi-shield-check', $gaAnonymizeIp);
|
||||
|
||||
$html .= ' <div class="alert alert-warning small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
|
||||
$html .= ' Recomendado activar para cumplir con GDPR/RGPD';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAdSenseGroup(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-badge-ad me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Google AdSense';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$publisherId = $this->renderer->getFieldValue($componentId, 'adsense', 'adsense_publisher_id', '');
|
||||
$html .= $this->buildTextInput('themeSettingsAdsensePublisherId', 'Publisher ID', 'bi-key', $publisherId);
|
||||
|
||||
$html .= ' <div class="form-text small mb-2">Formato: ca-pub-1234567890123456</div>';
|
||||
|
||||
$autoAds = $this->renderer->getFieldValue($componentId, 'adsense', 'adsense_auto_ads', false);
|
||||
$html .= $this->buildSwitch('themeSettingsAdsenseAutoAds', 'Activar Auto Ads', 'bi-magic', $autoAds);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' Auto Ads permite que Google coloque anuncios automaticamente en las mejores ubicaciones.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCustomCodeGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-code-slash me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Codigo Personalizado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$customCss = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_css', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomCss', 'CSS Personalizado', 'bi-filetype-css', $customCss, 'Se inyecta en wp_head. No incluir etiquetas <style>');
|
||||
|
||||
$customJsHeader = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_header', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomJsHeader', 'JavaScript en Header', 'bi-filetype-js', $customJsHeader, 'Se inyecta en wp_head. No incluir etiquetas <script>');
|
||||
|
||||
$customJsFooter = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_footer', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomJsFooter', 'JavaScript en Footer', 'bi-filetype-js', $customJsFooter, 'Se inyecta en wp_footer. No incluir etiquetas <script>');
|
||||
|
||||
$html .= ' <div class="alert alert-danger small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-octagon me-1"></i>';
|
||||
$html .= ' <strong>Advertencia:</strong> El codigo personalizado puede afectar el rendimiento y seguridad del sitio.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private function buildSwitch(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = ' <div class="form-check form-switch mb-2">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextInput(string $id, string $label, string $icon, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextareaCode(string $id, string $label, string $icon, mixed $value, string $helpText = ''): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea class="form-control form-control-sm font-monospace" id="' . esc_attr($id) . '" rows="4" style="font-size: 0.85em;">' . esc_textarea($value) . '</textarea>';
|
||||
if (!empty($helpText)) {
|
||||
$html .= ' <div class="form-text small">' . $helpText . '</div>';
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza un valor a string para inputs de formulario
|
||||
*/
|
||||
private function normalizeStringValue(mixed $value): string
|
||||
{
|
||||
if ($value === false) {
|
||||
return '0';
|
||||
}
|
||||
if ($value === true) {
|
||||
return '1';
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Top Notification Bar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class TopNotificationBarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'top-notification-bar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'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'],
|
||||
|
||||
// Content
|
||||
'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'],
|
||||
|
||||
// Colors
|
||||
'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'],
|
||||
|
||||
// Spacing
|
||||
'topBarFontSize' => ['group' => 'spacing', 'attribute' => 'font_size'],
|
||||
'topBarPadding' => ['group' => 'spacing', 'attribute' => 'padding'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
Public/.gitkeep
Normal file
0
Public/.gitkeep
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
777
Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
Normal file
777
Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
Normal file
@@ -0,0 +1,777 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza el modal de contacto para el boton Let's Talk
|
||||
* Usa la misma configuracion y webhook que el formulario de seccion
|
||||
*/
|
||||
public function renderModal(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
$css = $this->generateModalCSS($data);
|
||||
$html = $this->buildModalHTML($data);
|
||||
$js = $this->buildModalJS($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar CSS para el modal
|
||||
*/
|
||||
private function generateModalCSS(array $data): string
|
||||
{
|
||||
$colors = $data['colors'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Modal header con gradiente del tema
|
||||
$cssRules[] = $this->cssGenerator->generate('#contactModal .modal-header', [
|
||||
'background' => 'linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%)',
|
||||
'border-bottom' => 'none',
|
||||
'padding' => '1.5rem',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('#contactModal .modal-title', [
|
||||
'color' => '#ffffff',
|
||||
'font-weight' => '600',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('#contactModal .btn-close', [
|
||||
'filter' => 'brightness(0) invert(1)',
|
||||
]);
|
||||
|
||||
// Modal body
|
||||
$cssRules[] = $this->cssGenerator->generate('#contactModal .modal-body', [
|
||||
'padding' => '2rem',
|
||||
]);
|
||||
|
||||
// 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('#contactModal .form-control', [
|
||||
'border-color' => $inputBorderColor,
|
||||
'border-radius' => $inputBorderRadius,
|
||||
'transition' => "all {$transitionDuration} ease",
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('#contactModal .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('#contactModal .btn-modal-submit', [
|
||||
'background-color' => $buttonBgColor,
|
||||
'color' => $buttonTextColor,
|
||||
'font-weight' => '600',
|
||||
'padding' => $buttonPadding,
|
||||
'border' => 'none',
|
||||
'border-radius' => $buttonBorderRadius,
|
||||
'transition' => "all {$transitionDuration} ease",
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('#contactModal .btn-modal-submit:hover', [
|
||||
'background-color' => $buttonHoverBg,
|
||||
'color' => $buttonTextColor,
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('#contactModal .btn-modal-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('#contactModal .alert-success', [
|
||||
'background-color' => $successBgColor,
|
||||
'color' => $successTextColor,
|
||||
'border-color' => $successBgColor,
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('#contactModal .alert-danger', [
|
||||
'background-color' => $errorBgColor,
|
||||
'color' => $errorTextColor,
|
||||
'border-color' => $errorBgColor,
|
||||
]);
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar HTML del modal
|
||||
*/
|
||||
private function buildModalHTML(array $data): string
|
||||
{
|
||||
$content = $data['content'] ?? [];
|
||||
$formLabels = $data['form_labels'] ?? [];
|
||||
$effects = $data['visual_effects'] ?? [];
|
||||
|
||||
// Content
|
||||
$sectionTitle = $content['section_title'] ?? '¿Tienes alguna pregunta?';
|
||||
$submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
|
||||
$submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
|
||||
|
||||
// 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';
|
||||
|
||||
// Nonce for AJAX security
|
||||
$nonce = wp_create_nonce('roi_contact_form_nonce');
|
||||
|
||||
$html = '<div class="modal fade" id="contactModal" tabindex="-1" aria-labelledby="contactModalLabel" aria-hidden="true">';
|
||||
$html .= '<div class="modal-dialog modal-dialog-centered modal-lg">';
|
||||
$html .= '<div class="modal-content">';
|
||||
|
||||
// Modal Header
|
||||
$html .= '<div class="modal-header">';
|
||||
$html .= '<h5 class="modal-title" id="contactModalLabel">';
|
||||
$html .= '<i class="bi bi-chat-dots-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= esc_html($sectionTitle);
|
||||
$html .= '</h5>';
|
||||
$html .= '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Modal Body
|
||||
$html .= '<div class="modal-body">';
|
||||
$html .= sprintf('<form id="roiContactModalForm" 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="roiModalFullName" 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="roiModalCompany" 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="roiModalWhatsapp" 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="roiModalEmail" name="email" placeholder="%s" required>',
|
||||
esc_attr($emailPlaceholder)
|
||||
);
|
||||
$html .= '</div>';
|
||||
|
||||
// Message field
|
||||
$html .= '<div class="col-12">';
|
||||
$html .= sprintf(
|
||||
'<textarea class="form-control" id="roiModalMessage" 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-modal-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="roiContactModalMessage" class="col-12 mt-2 alert" style="display: none;"></div>';
|
||||
|
||||
$html .= '</div>'; // .row g-3
|
||||
$html .= '</form>';
|
||||
$html .= '</div>'; // .modal-body
|
||||
|
||||
$html .= '</div>'; // .modal-content
|
||||
$html .= '</div>'; // .modal-dialog
|
||||
$html .= '</div>'; // .modal
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar JS para el modal
|
||||
*/
|
||||
private function buildModalJS(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...';
|
||||
|
||||
// AJAX URL for WordPress
|
||||
$ajaxUrl = admin_url('admin-ajax.php');
|
||||
|
||||
$js = <<<JS
|
||||
(function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('roiContactModalForm');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const messageDiv = document.getElementById('roiContactModalMessage');
|
||||
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();
|
||||
|
||||
// Cerrar modal despues de 2 segundos en exito
|
||||
setTimeout(function() {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('contactModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
messageDiv.style.display = 'none';
|
||||
}, 2000);
|
||||
} 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 modal 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;
|
||||
}
|
||||
});
|
||||
|
||||
// Limpiar formulario cuando se cierra el modal
|
||||
const contactModal = document.getElementById('contactModal');
|
||||
if (contactModal) {
|
||||
contactModal.addEventListener('hidden.bs.modal', function() {
|
||||
form.reset();
|
||||
const messageDiv = document.getElementById('roiContactModalMessage');
|
||||
if (messageDiv) {
|
||||
messageDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
JS;
|
||||
|
||||
return $js;
|
||||
}
|
||||
}
|
||||
290
Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
Normal file
290
Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
Normal file
@@ -0,0 +1,290 @@
|
||||
<?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/Link
|
||||
$iconHtml = !empty($buttonIcon)
|
||||
? sprintf('<i class="%s"></i>', esc_attr($buttonIcon))
|
||||
: '';
|
||||
|
||||
// Use <a> for link action, <button> for modal/scroll
|
||||
if ($buttonAction === 'link') {
|
||||
$html .= sprintf(
|
||||
'<a href="%s" class="btn btn-cta-box">%s%s</a>',
|
||||
esc_url($buttonLink),
|
||||
$iconHtml,
|
||||
esc_html($buttonText)
|
||||
);
|
||||
} else {
|
||||
$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)
|
||||
);
|
||||
}
|
||||
}
|
||||
198
Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
Normal file
198
Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?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'] ?? [];
|
||||
$spacing = $data['spacing'] ?? [];
|
||||
$visibility = $data['visibility'] ?? [];
|
||||
|
||||
$cssRules = [];
|
||||
|
||||
// Container values
|
||||
$gradientStart = $colors['gradient_start'] ?? '#FF8600';
|
||||
$gradientEnd = $colors['gradient_end'] ?? '#FFB800';
|
||||
$gradientAngle = $effects['gradient_angle'] ?? '135deg';
|
||||
$borderRadius = $effects['border_radius'] ?? '12px';
|
||||
$boxShadow = $effects['box_shadow'] ?? '0 8px 24px rgba(255, 133, 0, 0.3)';
|
||||
$containerPadding = $spacing['container_padding'] ?? '2rem';
|
||||
|
||||
// Button values
|
||||
$buttonBgColor = $colors['button_bg_color'] ?? '#FF8600';
|
||||
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
|
||||
$buttonHoverBgColor = $colors['button_hover_bg'] ?? '#e67a00';
|
||||
$buttonBorderRadius = $effects['button_border_radius'] ?? '8px';
|
||||
|
||||
// Container - gradient background with box-shadow and border-radius
|
||||
$cssRules[] = $this->cssGenerator->generate('.cta-post-container', [
|
||||
'background' => "linear-gradient({$gradientAngle}, {$gradientStart} 0%, {$gradientEnd} 100%)",
|
||||
'box-shadow' => $boxShadow,
|
||||
'border-radius' => $borderRadius,
|
||||
'padding' => $containerPadding,
|
||||
]);
|
||||
|
||||
// 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: {$buttonBorderRadius};
|
||||
transition: all 0.3s ease;
|
||||
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 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="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,203 @@
|
||||
<?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 y sanitizar campos
|
||||
$email = sanitize_email($_POST['email'] ?? '');
|
||||
$name = sanitize_text_field($_POST['name'] ?? '');
|
||||
$whatsapp = sanitize_text_field($_POST['whatsapp'] ?? '');
|
||||
|
||||
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,
|
||||
'name' => $name,
|
||||
'whatsapp' => $whatsapp,
|
||||
'source' => 'newsletter-footer',
|
||||
'pageUrl' => sanitize_url($_POST['pageUrl'] ?? ''),
|
||||
'pageTitle' => sanitize_text_field($_POST['pageTitle'] ?? ''),
|
||||
'timestamp' => current_time('c'),
|
||||
'timezone' => wp_timezone_string(),
|
||||
'siteName' => get_bloginfo('name'),
|
||||
'siteUrl' => home_url(),
|
||||
];
|
||||
|
||||
// Debug: Log payload enviado
|
||||
error_log('ROI Theme Newsletter: Enviando a webhook - ' . wp_json_encode($payload));
|
||||
|
||||
// 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
|
||||
{
|
||||
error_log('ROI Theme Newsletter: Webhook URL - ' . $url);
|
||||
|
||||
$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)) {
|
||||
error_log('ROI Theme Newsletter: WP Error - ' . $response->get_error_message());
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $response->get_error_message()
|
||||
];
|
||||
}
|
||||
|
||||
$statusCode = wp_remote_retrieve_response_code($response);
|
||||
$responseBody = wp_remote_retrieve_body($response);
|
||||
|
||||
error_log('ROI Theme Newsletter: Response Code - ' . $statusCode);
|
||||
error_log('ROI Theme Newsletter: Response Body - ' . $responseBody);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
449
Public/Footer/Infrastructure/Ui/FooterRenderer.php
Normal file
449
Public/Footer/Infrastructure/Ui/FooterRenderer.php
Normal file
@@ -0,0 +1,449 @@
|
||||
<?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,
|
||||
]);
|
||||
|
||||
// Widget 1B spacing
|
||||
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-widget-1b', [
|
||||
'margin-top' => '1.5rem',
|
||||
]);
|
||||
|
||||
// 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'] ?? [];
|
||||
$widget1b = $data['widget_1b'] ?? [];
|
||||
$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');
|
||||
$widget1bTitle = esc_html($widget1b['widget_1b_title'] ?? 'Bases de datos');
|
||||
$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.');
|
||||
$newsletterNamePlaceholder = esc_attr($newsletter['newsletter_name_placeholder'] ?? 'Nombre');
|
||||
$newsletterEmailPlaceholder = esc_attr($newsletter['newsletter_email_placeholder'] ?? 'Email');
|
||||
$newsletterWhatsappPlaceholder = esc_attr($newsletter['newsletter_whatsapp_placeholder'] ?? 'WhatsApp');
|
||||
$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">';
|
||||
|
||||
// Columna 1: Widget 1 + Widget 1B
|
||||
if ($widget1Visible) {
|
||||
$html .= '<div class="footer-column footer-column-1">';
|
||||
|
||||
// Widget 1
|
||||
$html .= '<div class="footer-widget footer-widget-menu">';
|
||||
$html .= '<h5 class="widget-title">' . $widget1Title . '</h5>';
|
||||
$html .= $this->renderMenu('footer_menu_1');
|
||||
$html .= '</div>';
|
||||
|
||||
// Widget 1B - Solo si tiene menu asignado
|
||||
if (has_nav_menu('footer_menu_4')) {
|
||||
$html .= '<div class="footer-widget footer-widget-menu footer-widget-1b">';
|
||||
$html .= '<h5 class="widget-title">' . $widget1bTitle . '</h5>';
|
||||
$html .= $this->renderMenu('footer_menu_4');
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$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="text" name="name" class="newsletter-input" placeholder="' . $newsletterNamePlaceholder . '">';
|
||||
$html .= '<input type="email" name="email" class="newsletter-input" placeholder="' . $newsletterEmailPlaceholder . '" required>';
|
||||
$html .= '<input type="tel" name="whatsapp" class="newsletter-input" placeholder="' . $newsletterWhatsappPlaceholder . '">';
|
||||
$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);
|
||||
formData.append('pageUrl', window.location.href);
|
||||
formData.append('pageTitle', document.title);
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
478
Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php
Normal file
478
Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php
Normal file
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\herosection\infrastructure\ui;
|
||||
|
||||
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
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$classes = $this->buildSectionClasses($data);
|
||||
$styles = $this->buildInlineStyles($data);
|
||||
|
||||
$html = sprintf(
|
||||
'<div class="%s"%s>',
|
||||
esc_attr($classes),
|
||||
$styles ? ' style="' . esc_attr($styles) . '"' : ''
|
||||
);
|
||||
|
||||
$html .= '<div class="container">';
|
||||
|
||||
// Categories badges
|
||||
if ($this->shouldShowCategories($data)) {
|
||||
$html .= $this->buildCategoriesBadges($data);
|
||||
}
|
||||
|
||||
// Title
|
||||
$html .= $this->buildTitle($data);
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Custom styles
|
||||
$html .= $this->buildCustomStyles($data);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return isset($data['visibility']['is_enabled']) &&
|
||||
$data['visibility']['is_enabled'] === 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();
|
||||
|
||||
case 'posts':
|
||||
return is_single() && get_post_type() === 'post';
|
||||
|
||||
case 'pages':
|
||||
return is_page();
|
||||
|
||||
case 'custom':
|
||||
$postTypes = $data['visibility']['custom_post_types'] ?? '';
|
||||
$allowedTypes = array_map('trim', explode(',', $postTypes));
|
||||
return in_array(get_post_type(), $allowedTypes, true);
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldShowCategories(array $data): bool
|
||||
{
|
||||
return isset($data['categories']['show_categories']) &&
|
||||
$data['categories']['show_categories'] === true;
|
||||
}
|
||||
|
||||
private function buildSectionClasses(array $data): string
|
||||
{
|
||||
$classes = ['container-fluid', 'hero-title'];
|
||||
|
||||
$paddingClass = $this->getPaddingClass($data['styles']['padding_vertical'] ?? 'normal');
|
||||
$classes[] = $paddingClass;
|
||||
|
||||
$marginClass = $this->getMarginClass($data['styles']['margin_bottom'] ?? 'normal');
|
||||
if ($marginClass) {
|
||||
$classes[] = $marginClass;
|
||||
}
|
||||
|
||||
return implode(' ', $classes);
|
||||
}
|
||||
|
||||
private function getPaddingClass(string $padding): string
|
||||
{
|
||||
$paddings = [
|
||||
'compact' => 'py-3',
|
||||
'normal' => 'py-5',
|
||||
'spacious' => 'py-6',
|
||||
'extra-spacious' => 'py-7'
|
||||
];
|
||||
|
||||
return $paddings[$padding] ?? 'py-5';
|
||||
}
|
||||
|
||||
private function getMarginClass(string $margin): string
|
||||
{
|
||||
$margins = [
|
||||
'none' => '',
|
||||
'small' => 'mb-2',
|
||||
'normal' => 'mb-4',
|
||||
'large' => 'mb-5'
|
||||
];
|
||||
|
||||
return $margins[$margin] ?? 'mb-4';
|
||||
}
|
||||
|
||||
private function buildInlineStyles(array $data): string
|
||||
{
|
||||
$styles = [];
|
||||
$backgroundType = $data['styles']['background_type'] ?? 'gradient';
|
||||
|
||||
switch ($backgroundType) {
|
||||
case 'color':
|
||||
$bgColor = $data['styles']['background_color'] ?? '#1e3a5f';
|
||||
$styles[] = "background-color: {$bgColor}";
|
||||
break;
|
||||
|
||||
case 'gradient':
|
||||
$startColor = $data['styles']['gradient_start_color'] ?? '#1e3a5f';
|
||||
$endColor = $data['styles']['gradient_end_color'] ?? '#2c5282';
|
||||
$angle = $data['styles']['gradient_angle'] ?? 135;
|
||||
$styles[] = "background: linear-gradient({$angle}deg, {$startColor}, {$endColor})";
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
$imageUrl = $data['styles']['background_image_url'] ?? '';
|
||||
if (!empty($imageUrl)) {
|
||||
$styles[] = "background-image: url('" . esc_url($imageUrl) . "')";
|
||||
$styles[] = "background-size: cover";
|
||||
$styles[] = "background-position: center";
|
||||
$styles[] = "background-repeat: no-repeat";
|
||||
|
||||
if (isset($data['styles']['background_overlay']) && $data['styles']['background_overlay']) {
|
||||
$opacity = ($data['styles']['overlay_opacity'] ?? 60) / 100;
|
||||
$styles[] = "position: relative";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Text color
|
||||
if (!empty($data['styles']['text_color'])) {
|
||||
$styles[] = 'color: ' . $data['styles']['text_color'];
|
||||
}
|
||||
|
||||
return implode('; ', $styles);
|
||||
}
|
||||
|
||||
private function buildCategoriesBadges(array $data): string
|
||||
{
|
||||
$categories = $this->getCategories($data);
|
||||
|
||||
if (empty($categories)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$maxCategories = $data['categories']['max_categories'] ?? 5;
|
||||
$categories = array_slice($categories, 0, $maxCategories);
|
||||
|
||||
$alignment = $data['categories']['categories_alignment'] ?? 'center';
|
||||
$alignmentClasses = [
|
||||
'left' => 'justify-content-start',
|
||||
'center' => 'justify-content-center',
|
||||
'right' => 'justify-content-end'
|
||||
];
|
||||
$alignmentClass = $alignmentClasses[$alignment] ?? 'justify-content-center';
|
||||
|
||||
$icon = $data['categories']['category_icon'] ?? 'bi-folder-fill';
|
||||
if (strpos($icon, 'bi-') !== 0) {
|
||||
$icon = 'bi-' . $icon;
|
||||
}
|
||||
|
||||
$html = sprintf('<div class="mb-3 d-flex %s">', esc_attr($alignmentClass));
|
||||
$html .= '<div class="d-flex gap-2 flex-wrap justify-content-center">';
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$html .= sprintf(
|
||||
'<a href="%s" class="category-badge category-badge-hero"><i class="bi %s me-1"></i>%s</a>',
|
||||
esc_url($category['url']),
|
||||
esc_attr($icon),
|
||||
esc_html($category['name'])
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getCategories(array $data): array
|
||||
{
|
||||
$source = $data['categories']['categories_source'] ?? 'post_categories';
|
||||
|
||||
switch ($source) {
|
||||
case 'post_categories':
|
||||
return $this->getPostCategories();
|
||||
|
||||
case 'post_tags':
|
||||
return $this->getPostTags();
|
||||
|
||||
case 'custom_taxonomy':
|
||||
$taxonomy = $data['categories']['custom_taxonomy_name'] ?? '';
|
||||
return $this->getCustomTaxonomyTerms($taxonomy);
|
||||
|
||||
case 'custom_list':
|
||||
$list = $data['categories']['custom_categories_list'] ?? '';
|
||||
return $this->parseCustomCategoriesList($list);
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function getPostCategories(): array
|
||||
{
|
||||
$categories = get_the_category();
|
||||
if (empty($categories)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($categories as $category) {
|
||||
$result[] = [
|
||||
'name' => $category->name,
|
||||
'url' => get_category_link($category->term_id)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getPostTags(): array
|
||||
{
|
||||
$tags = get_the_tags();
|
||||
if (empty($tags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($tags as $tag) {
|
||||
$result[] = [
|
||||
'name' => $tag->name,
|
||||
'url' => get_tag_link($tag->term_id)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getCustomTaxonomyTerms(string $taxonomy): array
|
||||
{
|
||||
if (empty($taxonomy)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$terms = get_the_terms(get_the_ID(), $taxonomy);
|
||||
if (empty($terms) || is_wp_error($terms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($terms as $term) {
|
||||
$result[] = [
|
||||
'name' => $term->name,
|
||||
'url' => get_term_link($term)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseCustomCategoriesList(string $list): array
|
||||
{
|
||||
if (empty($list)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$lines = explode("\n", $list);
|
||||
$result = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode('|', $line);
|
||||
if (count($parts) >= 2) {
|
||||
$result[] = [
|
||||
'name' => trim($parts[0]),
|
||||
'url' => trim($parts[1])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function buildTitle(array $data): string
|
||||
{
|
||||
$titleText = $this->getTitleText($data);
|
||||
|
||||
if (empty($titleText)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$titleTag = $data['title']['title_tag'] ?? 'h1';
|
||||
$titleClasses = $data['title']['title_classes'] ?? 'display-5 fw-bold';
|
||||
$alignment = $data['title']['title_alignment'] ?? 'center';
|
||||
|
||||
$alignmentClasses = [
|
||||
'left' => 'text-start',
|
||||
'center' => 'text-center',
|
||||
'right' => 'text-end'
|
||||
];
|
||||
$alignmentClass = $alignmentClasses[$alignment] ?? 'text-center';
|
||||
|
||||
$classes = trim($titleClasses . ' ' . $alignmentClass);
|
||||
|
||||
$titleStyle = '';
|
||||
if (isset($data['title']['enable_gradient']) && $data['title']['enable_gradient']) {
|
||||
$titleStyle = $this->buildGradientStyle($data);
|
||||
$classes .= ' roi-gradient-text';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<%s class="%s"%s>%s</%s>',
|
||||
esc_attr($titleTag),
|
||||
esc_attr($classes),
|
||||
$titleStyle ? ' style="' . esc_attr($titleStyle) . '"' : '',
|
||||
esc_html($titleText),
|
||||
esc_attr($titleTag)
|
||||
);
|
||||
}
|
||||
|
||||
private function getTitleText(array $data): string
|
||||
{
|
||||
$source = $data['title']['title_source'] ?? 'post_title';
|
||||
|
||||
switch ($source) {
|
||||
case 'post_title':
|
||||
return get_the_title();
|
||||
|
||||
case 'custom_field':
|
||||
$fieldName = $data['title']['custom_field_name'] ?? '';
|
||||
if (!empty($fieldName)) {
|
||||
$value = get_post_meta(get_the_ID(), $fieldName, true);
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
return '';
|
||||
|
||||
case 'custom_text':
|
||||
return $data['title']['custom_text'] ?? '';
|
||||
|
||||
default:
|
||||
return get_the_title();
|
||||
}
|
||||
}
|
||||
|
||||
private function buildGradientStyle(array $data): string
|
||||
{
|
||||
$startColor = $data['title']['gradient_color_start'] ?? '#1e3a5f';
|
||||
$endColor = $data['title']['gradient_color_end'] ?? '#FF8600';
|
||||
$direction = $data['title']['gradient_direction'] ?? 'to-right';
|
||||
|
||||
$directions = [
|
||||
'to-right' => 'to right',
|
||||
'to-left' => 'to left',
|
||||
'to-bottom' => 'to bottom',
|
||||
'to-top' => 'to top',
|
||||
'diagonal' => '135deg'
|
||||
];
|
||||
|
||||
$gradientDirection = $directions[$direction] ?? 'to right';
|
||||
|
||||
return "background: linear-gradient({$gradientDirection}, {$startColor}, {$endColor}); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;";
|
||||
}
|
||||
|
||||
private function buildCustomStyles(array $data): string
|
||||
{
|
||||
$badgeBg = $data['styles']['category_badge_background'] ?? 'rgba(255, 255, 255, 0.2)';
|
||||
$badgeTextColor = $data['styles']['category_badge_text_color'] ?? '#FFFFFF';
|
||||
$badgeBlur = isset($data['styles']['category_badge_blur']) && $data['styles']['category_badge_blur'];
|
||||
|
||||
$blurStyle = $badgeBlur ? 'backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);' : '';
|
||||
|
||||
$overlayStyle = '';
|
||||
if (($data['styles']['background_type'] ?? '') === 'image' &&
|
||||
isset($data['styles']['background_overlay']) &&
|
||||
$data['styles']['background_overlay']) {
|
||||
$opacity = ($data['styles']['overlay_opacity'] ?? 60) / 100;
|
||||
$overlayStyle = <<<CSS
|
||||
.hero-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, {$opacity});
|
||||
z-index: 0;
|
||||
}
|
||||
.hero-title > .container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
CSS;
|
||||
}
|
||||
|
||||
return <<<STYLES
|
||||
<style>
|
||||
.category-badge-hero {
|
||||
background-color: {$badgeBg};
|
||||
color: {$badgeTextColor};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 2rem;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
{$blurStyle}
|
||||
}
|
||||
.category-badge-hero:hover {
|
||||
background-color: rgba(255, 134, 0, 0.3);
|
||||
color: {$badgeTextColor};
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.roi-gradient-text {
|
||||
display: inline-block;
|
||||
}
|
||||
{$overlayStyle}
|
||||
</style>
|
||||
STYLES;
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'hero-section';
|
||||
}
|
||||
}
|
||||
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,135 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ThemeSettings\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
|
||||
use ROITheme\Public\ThemeSettings\Infrastructure\Ui\ThemeSettingsRenderer;
|
||||
|
||||
/**
|
||||
* ThemeSettingsInjector
|
||||
*
|
||||
* Servicio que inyecta las configuraciones globales del tema
|
||||
* en los hooks de WordPress (wp_head y wp_footer).
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Registrar hooks de WordPress
|
||||
* - Obtener configuracion de theme-settings desde BD
|
||||
* - Delegar renderizado a ThemeSettingsRenderer
|
||||
* - Inyectar contenido en los hooks correspondientes
|
||||
*
|
||||
* @package ROITheme\Public\ThemeSettings\Infrastructure\Services
|
||||
*/
|
||||
final class ThemeSettingsInjector
|
||||
{
|
||||
private const COMPONENT_NAME = 'theme-settings';
|
||||
|
||||
/**
|
||||
* @param ComponentSettingsRepositoryInterface $repository Repositorio para leer configuraciones
|
||||
* @param ThemeSettingsRenderer $renderer Renderer para generar contenido
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ComponentSettingsRepositoryInterface $repository,
|
||||
private readonly ThemeSettingsRenderer $renderer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registra los hooks de WordPress para inyeccion
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Inyectar en wp_head con prioridad alta para GA y CSS
|
||||
add_action('wp_head', [$this, 'injectHeadContent'], 5);
|
||||
|
||||
// Inyectar en wp_footer con prioridad baja (al final)
|
||||
add_action('wp_footer', [$this, 'injectFooterContent'], 99);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inyecta contenido en wp_head
|
||||
*
|
||||
* Callback para el hook wp_head.
|
||||
* Genera y muestra: Google Analytics, Custom CSS, Custom JS Header
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function injectHeadContent(): void
|
||||
{
|
||||
try {
|
||||
$settings = $this->getSettings();
|
||||
|
||||
if (empty($settings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $this->renderer->renderHeadContent($settings);
|
||||
|
||||
if (!empty($content)) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $content;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logError('Error injecting head content', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inyecta contenido en wp_footer
|
||||
*
|
||||
* Callback para el hook wp_footer.
|
||||
* Genera y muestra: Custom JS Footer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function injectFooterContent(): void
|
||||
{
|
||||
try {
|
||||
$settings = $this->getSettings();
|
||||
|
||||
if (empty($settings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $this->renderer->renderFooterContent($settings);
|
||||
|
||||
if (!empty($content)) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $content;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logError('Error injecting footer content', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las configuraciones del componente theme-settings
|
||||
*
|
||||
* @return array Configuraciones agrupadas o array vacio si no hay
|
||||
*/
|
||||
private function getSettings(): array
|
||||
{
|
||||
return $this->repository->getComponentSettings(self::COMPONENT_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra errores en el log de WordPress
|
||||
*
|
||||
* @param string $message Mensaje de error
|
||||
* @param \Throwable $e Excepcion
|
||||
* @return void
|
||||
*/
|
||||
private function logError(string $message, \Throwable $e): void
|
||||
{
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log(sprintf(
|
||||
'ROI Theme - ThemeSettingsInjector: %s - %s in %s:%d',
|
||||
$message,
|
||||
$e->getMessage(),
|
||||
$e->getFile(),
|
||||
$e->getLine()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
344
Public/ThemeSettings/Infrastructure/Ui/ThemeSettingsRenderer.php
Normal file
344
Public/ThemeSettings/Infrastructure/Ui/ThemeSettingsRenderer.php
Normal file
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ThemeSettings\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* ThemeSettingsRenderer
|
||||
*
|
||||
* Renderizador del componente Theme Settings.
|
||||
* A diferencia de otros componentes, no renderiza HTML visual
|
||||
* sino que genera codigo para inyectar en wp_head y wp_footer.
|
||||
*
|
||||
* NOTA: Este es un componente especial que NO requiere:
|
||||
* - CSSGeneratorInterface (no genera CSS, solo inyecta CSS del usuario)
|
||||
* - Grupo visibility (siempre esta activo, configuraciones globales)
|
||||
* - Metodo getVisibilityClasses (no es un componente visual)
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Generar script de Google Analytics
|
||||
* - Generar script de Google AdSense Auto Ads
|
||||
* - Generar CSS personalizado
|
||||
* - Generar JavaScript para header
|
||||
* - Generar JavaScript para footer
|
||||
*
|
||||
* @package ROITheme\Public\ThemeSettings\Infrastructure\Ui
|
||||
*/
|
||||
final class ThemeSettingsRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Para este componente, render() no se usa directamente.
|
||||
* Se usan los metodos especificos: renderHeadContent() y renderFooterContent()
|
||||
*/
|
||||
public function render(Component $component): string
|
||||
{
|
||||
// Este componente no renderiza HTML visual
|
||||
// Los contenidos se inyectan via hooks wp_head y wp_footer
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'theme-settings';
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera contenido para wp_head
|
||||
*
|
||||
* Incluye:
|
||||
* - Google Analytics script (si configurado)
|
||||
* - Google AdSense Auto Ads script (si configurado)
|
||||
* - Custom CSS (si configurado)
|
||||
* - Custom JS Header (si configurado)
|
||||
*
|
||||
* @param array $data Datos del componente desde BD
|
||||
* @return string Contenido para wp_head
|
||||
*/
|
||||
public function renderHeadContent(array $data): string
|
||||
{
|
||||
// Validar visibilidad general
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$output = '';
|
||||
|
||||
// Google Analytics
|
||||
$gaOutput = $this->renderGoogleAnalytics($data);
|
||||
if (!empty($gaOutput)) {
|
||||
$output .= $gaOutput . "\n";
|
||||
}
|
||||
|
||||
// Google AdSense Auto Ads
|
||||
$adsenseOutput = $this->renderAdSenseAutoAds($data);
|
||||
if (!empty($adsenseOutput)) {
|
||||
$output .= $adsenseOutput . "\n";
|
||||
}
|
||||
|
||||
// Custom CSS
|
||||
$cssOutput = $this->renderCustomCSS($data);
|
||||
if (!empty($cssOutput)) {
|
||||
$output .= $cssOutput . "\n";
|
||||
}
|
||||
|
||||
// Custom JS Header
|
||||
$jsHeaderOutput = $this->renderCustomJSHeader($data);
|
||||
if (!empty($jsHeaderOutput)) {
|
||||
$output .= $jsHeaderOutput . "\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera contenido para wp_footer
|
||||
*
|
||||
* Incluye:
|
||||
* - Custom JS Footer (si configurado)
|
||||
*
|
||||
* @param array $data Datos del componente desde BD
|
||||
* @return string Contenido para wp_footer
|
||||
*/
|
||||
public function renderFooterContent(array $data): string
|
||||
{
|
||||
// Validar visibilidad general
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->renderCustomJSFooter($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si el componente esta habilitado
|
||||
*
|
||||
* NOTA: Theme Settings es un componente de configuracion global
|
||||
* que siempre esta activo. No tiene grupo visibility.
|
||||
* Si el usuario no quiere GA o CSS custom, simplemente deja
|
||||
* los campos vacios.
|
||||
*
|
||||
* @param array $data Datos del componente (no usado)
|
||||
* @return bool Siempre true
|
||||
*/
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
// Theme Settings siempre esta activo (configuraciones globales)
|
||||
// Los campos individuales se validan en sus metodos respectivos
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el script de Google Analytics
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Script de GA o vacio si no configurado
|
||||
*/
|
||||
private function renderGoogleAnalytics(array $data): string
|
||||
{
|
||||
$trackingId = trim($data['analytics']['ga_tracking_id'] ?? '');
|
||||
|
||||
if (empty($trackingId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si GA ya esta cargado por otro plugin
|
||||
if ($this->isGoogleAnalyticsLoaded()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$anonymizeIp = ($data['analytics']['ga_anonymize_ip'] ?? true) === true;
|
||||
|
||||
// Detectar tipo de ID (GA4 vs Universal Analytics)
|
||||
if (strpos($trackingId, 'G-') === 0) {
|
||||
// Google Analytics 4
|
||||
return $this->renderGA4Script($trackingId, $anonymizeIp);
|
||||
} elseif (strpos($trackingId, 'UA-') === 0) {
|
||||
// Universal Analytics (legacy)
|
||||
return $this->renderUniversalAnalyticsScript($trackingId, $anonymizeIp);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera script de Google Analytics 4
|
||||
*
|
||||
* @param string $trackingId ID de GA4 (G-XXXXXXXXXX)
|
||||
* @param bool $anonymizeIp Si anonimizar IP
|
||||
* @return string Script HTML
|
||||
*/
|
||||
private function renderGA4Script(string $trackingId, bool $anonymizeIp): string
|
||||
{
|
||||
$config = $anonymizeIp ? "{ 'anonymize_ip': true }" : '{}';
|
||||
|
||||
return sprintf(
|
||||
'<!-- Google Analytics 4 (ROI Theme) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=%1$s"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "%1$s", %2$s);
|
||||
</script>',
|
||||
esc_attr($trackingId),
|
||||
$config
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera script de Universal Analytics (legacy)
|
||||
*
|
||||
* @param string $trackingId ID de UA (UA-XXXXXXXX-X)
|
||||
* @param bool $anonymizeIp Si anonimizar IP
|
||||
* @return string Script HTML
|
||||
*/
|
||||
private function renderUniversalAnalyticsScript(string $trackingId, bool $anonymizeIp): string
|
||||
{
|
||||
$anonymizeConfig = $anonymizeIp ? "ga('set', 'anonymizeIp', true);" : '';
|
||||
|
||||
return sprintf(
|
||||
'<!-- Universal Analytics (ROI Theme) -->
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,"script","https://www.google-analytics.com/analytics.js","ga");
|
||||
ga("create", "%s", "auto");
|
||||
%s
|
||||
ga("send", "pageview");
|
||||
</script>',
|
||||
esc_attr($trackingId),
|
||||
$anonymizeConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si Google Analytics ya esta cargado
|
||||
*
|
||||
* @return bool True si ya esta cargado por otro plugin
|
||||
*/
|
||||
private function isGoogleAnalyticsLoaded(): bool
|
||||
{
|
||||
// Verificar plugins comunes de GA
|
||||
if (function_exists('gtag')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar si MonsterInsights esta activo
|
||||
if (class_exists('MonsterInsights_Lite') || class_exists('MonsterInsights')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar si Site Kit de Google esta activo
|
||||
if (class_exists('Google\Site_Kit\Plugin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el script de Google AdSense Auto Ads
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Script de AdSense o vacio si no configurado
|
||||
*/
|
||||
private function renderAdSenseAutoAds(array $data): string
|
||||
{
|
||||
$publisherId = trim($data['adsense']['adsense_publisher_id'] ?? '');
|
||||
$autoAdsEnabled = ($data['adsense']['adsense_auto_ads'] ?? false) === true;
|
||||
|
||||
// Solo mostrar si tiene publisher ID y auto ads esta activado
|
||||
if (empty($publisherId) || !$autoAdsEnabled) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar formato del publisher ID (ca-pub-XXXXXXXXXX)
|
||||
if (!preg_match('/^ca-pub-\d+$/', $publisherId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<!-- Google AdSense Auto Ads (ROI Theme) -->
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=%s" crossorigin="anonymous"></script>',
|
||||
esc_attr($publisherId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el CSS personalizado
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Bloque style o vacio si no hay CSS
|
||||
*/
|
||||
private function renderCustomCSS(array $data): string
|
||||
{
|
||||
$css = trim($data['custom_code']['custom_css'] ?? '');
|
||||
|
||||
if (empty($css)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<!-- Custom CSS (ROI Theme) -->
|
||||
<style id="roi-theme-custom-css">
|
||||
%s
|
||||
</style>',
|
||||
$css // No escapar CSS - usuario avanzado responsable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el JavaScript personalizado para header
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Bloque script o vacio si no hay JS
|
||||
*/
|
||||
private function renderCustomJSHeader(array $data): string
|
||||
{
|
||||
$js = trim($data['custom_code']['custom_js_header'] ?? '');
|
||||
|
||||
if (empty($js)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<!-- Custom JS Header (ROI Theme) -->
|
||||
<script id="roi-theme-custom-js-header">
|
||||
%s
|
||||
</script>',
|
||||
$js // No escapar JS - usuario avanzado responsable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el JavaScript personalizado para footer
|
||||
*
|
||||
* @param array $data Datos del componente
|
||||
* @return string Bloque script o vacio si no hay JS
|
||||
*/
|
||||
private function renderCustomJSFooter(array $data): string
|
||||
{
|
||||
$js = trim($data['custom_code']['custom_js_footer'] ?? '');
|
||||
|
||||
if (empty($js)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<!-- Custom JS Footer (ROI Theme) -->
|
||||
<script id="roi-theme-custom-js-footer">
|
||||
%s
|
||||
</script>',
|
||||
$js // No escapar JS - usuario avanzado responsable
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* Caja de llamada a la acción naranja en el sidebar
|
||||
* Abre el modal de contacto al hacer clic
|
||||
*
|
||||
* @package APUs_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
?>
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* Hero section con degradado azul para single posts
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
*/
|
||||
|
||||
if (!is_single()) {
|
||||
@@ -45,7 +45,7 @@ if (!is_single()) {
|
||||
<?php the_author(); ?>
|
||||
</span>
|
||||
<?php
|
||||
$reading_time = apus_get_reading_time();
|
||||
$reading_time = roi_get_reading_time();
|
||||
if ($reading_time) :
|
||||
?>
|
||||
<span class="hero-meta-separator">|</span>
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
?>
|
||||
@@ -12,7 +12,7 @@
|
||||
<section class="no-results not-found">
|
||||
|
||||
<header class="page-header">
|
||||
<h1 class="page-title"><?php esc_html_e( 'Nothing Found', 'apus-theme' ); ?></h1>
|
||||
<h1 class="page-title"><?php esc_html_e( 'Nothing Found', 'roi-theme' ); ?></h1>
|
||||
</header><!-- .page-header -->
|
||||
|
||||
<div class="page-content">
|
||||
@@ -23,7 +23,7 @@
|
||||
printf(
|
||||
'<p>' . wp_kses(
|
||||
/* translators: 1: link to WP admin new post page. */
|
||||
__( 'Ready to publish your first post? <a href="%1$s">Get started here</a>.', 'apus-theme' ),
|
||||
__( 'Ready to publish your first post? <a href="%1$s">Get started here</a>.', 'roi-theme' ),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
@@ -37,7 +37,7 @@
|
||||
?>
|
||||
|
||||
<!-- Search returned no results -->
|
||||
<p><?php esc_html_e( 'Sorry, but nothing matched your search terms. Please try again with some different keywords.', 'apus-theme' ); ?></p>
|
||||
<p><?php esc_html_e( 'Sorry, but nothing matched your search terms. Please try again with some different keywords.', 'roi-theme' ); ?></p>
|
||||
<?php
|
||||
get_search_form();
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
?>
|
||||
|
||||
<!-- Generic no content message -->
|
||||
<p><?php esc_html_e( 'It seems we can’t find what you’re looking for. Perhaps searching can help.', 'apus-theme' ); ?></p>
|
||||
<p><?php esc_html_e( 'It seems we can’t find what you’re looking for. Perhaps searching can help.', 'roi-theme' ); ?></p>
|
||||
<?php
|
||||
get_search_form();
|
||||
|
||||
42
TemplateParts/content-toc.php
Normal file
42
TemplateParts/content-toc.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Part: Table of Contents (TOC)
|
||||
*
|
||||
* Genera automáticamente TOC desde los H2 del post
|
||||
* HTML exacto del template original
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Solo mostrar TOC si estamos en single post
|
||||
if (!is_single()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener el contenido del post actual
|
||||
global $post;
|
||||
$post_content = $post->post_content;
|
||||
|
||||
// Aplicar filtros de WordPress al contenido
|
||||
$post_content = apply_filters('the_content', $post_content);
|
||||
|
||||
// Buscar todos los H2 con ID en el contenido
|
||||
preg_match_all('/<h2[^>]*id=["\']([^"\']*)["\'][^>]*>(.*?)<\/h2>/i', $post_content, $matches);
|
||||
|
||||
// Si no hay H2 con ID, no mostrar TOC
|
||||
if (empty($matches[1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generar el TOC con el HTML del template
|
||||
?>
|
||||
<div class="toc-container">
|
||||
<h4>Tabla de Contenido</h4>
|
||||
<ol class="list-unstyled toc-list">
|
||||
<?php foreach ($matches[1] as $index => $id) : ?>
|
||||
<?php $title = strip_tags($matches[2][$index]); ?>
|
||||
<li><a href="#<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
?>
|
||||
@@ -36,7 +36,7 @@
|
||||
// Posted by author
|
||||
printf(
|
||||
'<span class="byline"> %s <span class="author vcard"><a class="url fn n" href="%s">%s</a></span></span>',
|
||||
esc_html__( 'by', 'apus-theme' ),
|
||||
esc_html__( 'by', 'roi-theme' ),
|
||||
esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ),
|
||||
esc_html( get_the_author() )
|
||||
);
|
||||
@@ -45,9 +45,9 @@
|
||||
if ( ! post_password_required() && ( comments_open() || get_comments_number() ) ) :
|
||||
echo '<span class="comments-link">';
|
||||
comments_popup_link(
|
||||
esc_html__( 'Leave a comment', 'apus-theme' ),
|
||||
esc_html__( '1 Comment', 'apus-theme' ),
|
||||
esc_html__( '% Comments', 'apus-theme' )
|
||||
esc_html__( 'Leave a comment', 'roi-theme' ),
|
||||
esc_html__( '1 Comment', 'roi-theme' ),
|
||||
esc_html__( '% Comments', 'roi-theme' )
|
||||
);
|
||||
echo '</span>';
|
||||
endif;
|
||||
@@ -63,11 +63,11 @@
|
||||
<div class="post-thumbnail">
|
||||
<?php
|
||||
if ( is_singular() ) :
|
||||
the_post_thumbnail( 'apus-featured-large', array( 'alt' => the_title_attribute( array( 'echo' => false ) ) ) );
|
||||
the_post_thumbnail( 'roi-featured-large', array( 'alt' => the_title_attribute( array( 'echo' => false ) ) ) );
|
||||
else :
|
||||
?>
|
||||
<a href="<?php the_permalink(); ?>" aria-hidden="true" tabindex="-1">
|
||||
<?php the_post_thumbnail( 'apus-featured-medium', array( 'alt' => the_title_attribute( array( 'echo' => false ) ) ) ); ?>
|
||||
<?php the_post_thumbnail( 'roi-featured-medium', array( 'alt' => the_title_attribute( array( 'echo' => false ) ) ) ); ?>
|
||||
</a>
|
||||
<?php
|
||||
endif;
|
||||
@@ -84,7 +84,7 @@
|
||||
sprintf(
|
||||
wp_kses(
|
||||
/* translators: %s: Name of current post. Only visible to screen readers */
|
||||
__( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'apus-theme' ),
|
||||
__( 'Continue reading<span class="screen-reader-text"> "%s"</span>', 'roi-theme' ),
|
||||
array(
|
||||
'span' => array(
|
||||
'class' => array(),
|
||||
@@ -98,7 +98,7 @@
|
||||
// Display pagination for multi-page posts
|
||||
wp_link_pages(
|
||||
array(
|
||||
'before' => '<div class="page-links">' . esc_html__( 'Pages:', 'apus-theme' ),
|
||||
'before' => '<div class="page-links">' . esc_html__( 'Pages:', 'roi-theme' ),
|
||||
'after' => '</div>',
|
||||
)
|
||||
);
|
||||
@@ -112,21 +112,21 @@
|
||||
<footer class="entry-footer">
|
||||
<?php
|
||||
// Display categories
|
||||
$categories_list = get_the_category_list( esc_html__( ', ', 'apus-theme' ) );
|
||||
$categories_list = get_the_category_list( esc_html__( ', ', 'roi-theme' ) );
|
||||
if ( $categories_list ) :
|
||||
printf(
|
||||
'<span class="cat-links">%s %s</span>',
|
||||
esc_html__( 'Categories:', 'apus-theme' ),
|
||||
esc_html__( 'Categories:', 'roi-theme' ),
|
||||
$categories_list // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
);
|
||||
endif;
|
||||
|
||||
// Display tags
|
||||
$tags_list = get_the_tag_list( '', esc_html_x( ', ', 'list item separator', 'apus-theme' ) );
|
||||
$tags_list = get_the_tag_list( '', esc_html_x( ', ', 'list item separator', 'roi-theme' ) );
|
||||
if ( $tags_list ) :
|
||||
printf(
|
||||
'<span class="tags-links">%s %s</span>',
|
||||
esc_html__( 'Tags:', 'apus-theme' ),
|
||||
esc_html__( 'Tags:', 'roi-theme' ),
|
||||
$tags_list // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
);
|
||||
endif;
|
||||
@@ -136,7 +136,7 @@
|
||||
sprintf(
|
||||
wp_kses(
|
||||
/* translators: %s: Name of current post. Only visible to screen readers */
|
||||
__( 'Edit <span class="screen-reader-text">%s</span>', 'apus-theme' ),
|
||||
__( 'Edit <span class="screen-reader-text">%s</span>', 'roi-theme' ),
|
||||
array(
|
||||
'span' => array(
|
||||
'class' => array(),
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* Aparece debajo del TOC en single posts
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
*/
|
||||
|
||||
if (!is_single()) {
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* Modal activado por botón "Let's Talk" y CTA Box Sidebar
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
?>
|
||||
@@ -16,40 +16,40 @@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="contactModalLabel">
|
||||
<i class="bi bi-envelope-fill me-2" style="color: #FF8600;"></i>
|
||||
<?php esc_html_e( '¿Tienes alguna pregunta?', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( '¿Tienes alguna pregunta?', 'roi-theme' ); ?>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php esc_attr_e( 'Cerrar', 'apus-theme' ); ?>"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php esc_attr_e( 'Cerrar', 'roi-theme' ); ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-4">
|
||||
<?php esc_html_e( 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.', 'roi-theme' ); ?>
|
||||
</p>
|
||||
|
||||
<form id="modalContactForm">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="modalFullName" class="form-label"><?php esc_html_e( 'Nombre completo', 'apus-theme' ); ?> *</label>
|
||||
<label for="modalFullName" class="form-label"><?php esc_html_e( 'Nombre completo', 'roi-theme' ); ?> *</label>
|
||||
<input type="text" class="form-control" id="modalFullName" name="fullName" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="modalCompany" class="form-label"><?php esc_html_e( 'Empresa', 'apus-theme' ); ?></label>
|
||||
<label for="modalCompany" class="form-label"><?php esc_html_e( 'Empresa', 'roi-theme' ); ?></label>
|
||||
<input type="text" class="form-control" id="modalCompany" name="company">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="modalWhatsapp" class="form-label"><?php esc_html_e( 'WhatsApp', 'apus-theme' ); ?> *</label>
|
||||
<label for="modalWhatsapp" class="form-label"><?php esc_html_e( 'WhatsApp', 'roi-theme' ); ?> *</label>
|
||||
<input type="tel" class="form-control" id="modalWhatsapp" name="whatsapp" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="modalEmail" class="form-label"><?php esc_html_e( 'Correo electrónico', 'apus-theme' ); ?> *</label>
|
||||
<label for="modalEmail" class="form-label"><?php esc_html_e( 'Correo electrónico', 'roi-theme' ); ?> *</label>
|
||||
<input type="email" class="form-control" id="modalEmail" name="email" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="modalComments" class="form-label"><?php esc_html_e( '¿En qué podemos ayudarte?', 'apus-theme' ); ?></label>
|
||||
<label for="modalComments" class="form-label"><?php esc_html_e( '¿En qué podemos ayudarte?', 'roi-theme' ); ?></label>
|
||||
<textarea class="form-control" id="modalComments" name="comments" rows="4"></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-send-fill me-2"></i><?php esc_html_e( 'Enviar Mensaje', 'apus-theme' ); ?>
|
||||
<i class="bi bi-send-fill me-2"></i><?php esc_html_e( 'Enviar Mensaje', 'roi-theme' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div id="modalFormMessage" class="col-12 mt-2 alert" style="display: none;"></div>
|
||||
20
TemplateParts/top-notification-bar.php
Normal file
20
TemplateParts/top-notification-bar.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Top Notification Bar Component
|
||||
*
|
||||
* Barra de notificaciones superior del sitio
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
?>
|
||||
|
||||
<div class="top-notification-bar">
|
||||
<div class="container">
|
||||
<div class="d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-megaphone-fill me-2"></i>
|
||||
<span><strong>Nuevo:</strong> Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</span>
|
||||
<a href="#" class="ms-2 text-white text-decoration-underline">Ver Catálogo</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,538 +0,0 @@
|
||||
# ANÁLISIS: Problema Crítico de Duplicación de Valores por Defecto
|
||||
|
||||
**Fecha:** 2025-01-13
|
||||
**Severidad:** 🔴 ALTA - Problema de diseño arquitectónico
|
||||
**Tipo:** Violación del principio DRY (Don't Repeat Yourself)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 PROBLEMA IDENTIFICADO
|
||||
|
||||
El texto `"Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025."` está **duplicado en 7 ubicaciones diferentes**, lo que genera:
|
||||
|
||||
- ❌ **Difícil mantenimiento** - Cambiar requiere editar 7 archivos
|
||||
- ❌ **Alto riesgo de errores** - Fácil olvidar actualizar un archivo
|
||||
- ❌ **Inconsistencias** - Valores pueden desincronizarse
|
||||
- ❌ **No hay fuente única de verdad** - Múltiples definiciones de defaults
|
||||
|
||||
---
|
||||
|
||||
## 📍 UBICACIONES DE LA DUPLICACIÓN
|
||||
|
||||
### 1. **admin/assets/js/admin-app.js** (línea 357)
|
||||
```javascript
|
||||
document.getElementById('topBarMessageText').value = topBar.message_text || 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.';
|
||||
```
|
||||
**Propósito:** Fallback en JavaScript al renderizar el formulario
|
||||
**Problema:** Duplica el default que ya está en PHP
|
||||
|
||||
---
|
||||
|
||||
### 2. **admin/includes/sanitizers/class-topbar-sanitizer.php** (línea 37)
|
||||
```php
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
// ...
|
||||
'message_text' => 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
|
||||
// ...
|
||||
);
|
||||
}
|
||||
```
|
||||
**Propósito:** Define defaults del sanitizer
|
||||
**Problema:** ¿Por qué el sanitizer define defaults? Debería solo sanitizar.
|
||||
|
||||
---
|
||||
|
||||
### 3. **admin/includes/class-settings-manager.php** (línea 84)
|
||||
```php
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
// ...
|
||||
'components' => array(
|
||||
'top_bar' => array(
|
||||
'message_text' => 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
|
||||
// ...
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
**Propósito:** Define defaults centralizados del Settings Manager
|
||||
**Problema:** ⚠️ **DUPLICA lo que ya tiene el Sanitizer**
|
||||
|
||||
---
|
||||
|
||||
### 4. **admin/pages/main.php** (líneas 243-244, 495)
|
||||
|
||||
**Línea 243-244:**
|
||||
```html
|
||||
<textarea id="topBarMessageText"
|
||||
placeholder="Ej: Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025."
|
||||
required>Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</textarea>
|
||||
```
|
||||
|
||||
**Línea 495 (preview):**
|
||||
```html
|
||||
<span>Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</span>
|
||||
```
|
||||
|
||||
**Propósito:**
|
||||
- Placeholder del textarea
|
||||
- Valor inicial del textarea
|
||||
- Texto de preview
|
||||
|
||||
**Problema:** ❌ **TRIPLE duplicación en un solo archivo**
|
||||
|
||||
---
|
||||
|
||||
### 5. **admin/components/component-top-bar.php** (línea 190, aparece 2 veces)
|
||||
```html
|
||||
<textarea id="topBarMessageText"
|
||||
placeholder="Ej: Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025."
|
||||
required="">Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</textarea>
|
||||
```
|
||||
**Propósito:** Similar a main.php (placeholder + valor)
|
||||
**Problema:** ¿Por qué existe este archivo si main.php ya tiene el formulario?
|
||||
|
||||
---
|
||||
|
||||
### 6. **header.php** (línea 34)
|
||||
```php
|
||||
'message_text' => 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
|
||||
```
|
||||
**Propósito:** Fallback en el front-end del tema
|
||||
**Problema:** El front-end NO debería definir defaults, debería leerlos del Settings Manager
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ ANÁLISIS ARQUITECTÓNICO
|
||||
|
||||
### Arquitectura ACTUAL (Problemática)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CAPA 1: DEFAULTS DUPLICADOS (7 lugares) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ❌ TopBar Sanitizer::get_defaults() │
|
||||
│ ❌ Settings Manager::get_defaults() │
|
||||
│ ❌ admin-app.js (fallbacks en render) │
|
||||
│ ❌ main.php (placeholder + valor inicial + preview) │
|
||||
│ ❌ component-top-bar.php (placeholder + valor) │
|
||||
│ ❌ header.php (fallback front-end) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
🔴 PROBLEMA: No hay fuente única de verdad
|
||||
```
|
||||
|
||||
### Arquitectura CORRECTA (Propuesta)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ÚNICA FUENTE DE VERDAD │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ✅ Settings Manager::get_defaults() SOLAMENTE │
|
||||
│ - Define TODOS los defaults de TODOS los componentes │
|
||||
│ - Usa constantes PHP para valores reutilizables │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CONSUMIDORES (leen de Settings Manager) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ✅ TopBar Sanitizer → Llama Settings Manager::get_defaults() │
|
||||
│ ✅ admin-app.js → AJAX lee settings (ya con defaults merged) │
|
||||
│ ✅ main.php → Usa PHP para obtener defaults dinámicamente │
|
||||
│ ✅ header.php → Lee Settings Manager (NO define defaults) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 RAZONES DE LA DUPLICACIÓN
|
||||
|
||||
### 1. **Sanitizer vs Settings Manager** (Confusión de responsabilidades)
|
||||
|
||||
**Problema:**
|
||||
- `APUS_TopBar_Sanitizer::get_defaults()` define defaults
|
||||
- `APUS_Settings_Manager::get_defaults()` TAMBIÉN define defaults
|
||||
|
||||
**Pregunta:** ¿Por qué el SANITIZER define defaults?
|
||||
|
||||
**Responsabilidades correctas:**
|
||||
- ✅ **Sanitizer:** Solo SANITIZAR datos (validar, limpiar)
|
||||
- ✅ **Settings Manager:** Definir defaults, leer DB, hacer merge
|
||||
|
||||
**Solución:**
|
||||
- Eliminar `get_defaults()` del Sanitizer
|
||||
- Mantener solo en Settings Manager
|
||||
|
||||
---
|
||||
|
||||
### 2. **JavaScript con Fallbacks Hardcodeados**
|
||||
|
||||
**Código actual (admin-app.js:357):**
|
||||
```javascript
|
||||
topBar.message_text || 'Accede a más de 200,000...'
|
||||
```
|
||||
|
||||
**Problema:** JavaScript NO debería tener defaults hardcodeados.
|
||||
|
||||
**Solución:**
|
||||
Cuando JavaScript llama a AJAX para cargar settings, el Settings Manager YA hace merge con defaults:
|
||||
|
||||
```php
|
||||
// Settings Manager ya retorna datos con defaults merged
|
||||
public function get_settings() {
|
||||
$db_data = $this->db_manager->get_all_settings();
|
||||
$defaults = $this->get_defaults();
|
||||
return wp_parse_args($db_data, $defaults); // ← Merge automático
|
||||
}
|
||||
```
|
||||
|
||||
Por lo tanto, JavaScript NUNCA recibirá un `message_text` vacío. El fallback `|| 'Accede...'` es **innecesario**.
|
||||
|
||||
**Corrección:**
|
||||
```javascript
|
||||
// ANTES:
|
||||
document.getElementById('topBarMessageText').value = topBar.message_text || 'Accede...';
|
||||
|
||||
// DESPUÉS:
|
||||
document.getElementById('topBarMessageText').value = topBar.message_text;
|
||||
// ↑ Settings Manager YA hizo merge con defaults
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **HTML con Valores Hardcodeados** (main.php, component-top-bar.php)
|
||||
|
||||
**Código actual:**
|
||||
```html
|
||||
<textarea placeholder="Ej: Accede...">Accede...</textarea>
|
||||
```
|
||||
|
||||
**Problemas:**
|
||||
1. Placeholder hardcodeado
|
||||
2. Valor inicial hardcodeado
|
||||
3. Preview hardcodeado
|
||||
|
||||
**Solución:** Usar PHP para obtener defaults dinámicamente
|
||||
|
||||
```php
|
||||
<?php
|
||||
$settings_manager = new APUS_Settings_Manager();
|
||||
$defaults = $settings_manager->get_defaults();
|
||||
$default_message = $defaults['components']['top_bar']['message_text'];
|
||||
?>
|
||||
|
||||
<textarea
|
||||
id="topBarMessageText"
|
||||
placeholder="Ej: <?php echo esc_attr($default_message); ?>"
|
||||
><?php echo esc_html($default_message); ?></textarea>
|
||||
```
|
||||
|
||||
**Preview:**
|
||||
```html
|
||||
<span id="topBarPreview"><?php echo esc_html($default_message); ?></span>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. **component-top-bar.php vs main.php** (¿Duplicación de archivos?)
|
||||
|
||||
**Observación:**
|
||||
- `admin/pages/main.php` contiene el formulario del Top Bar
|
||||
- `admin/components/component-top-bar.php` TAMBIÉN contiene el formulario del Top Bar
|
||||
|
||||
**Pregunta:** ¿Por qué existen 2 archivos con el mismo formulario?
|
||||
|
||||
**Hipótesis:**
|
||||
1. **component-top-bar.php** es un archivo PHP modular (componente)
|
||||
2. **main.php** debería INCLUIR el componente, no duplicar el código
|
||||
|
||||
**Solución propuesta:**
|
||||
```php
|
||||
// main.php - Debería ser así:
|
||||
<div id="topBarTab" class="tab-pane fade show active">
|
||||
<?php require_once APUS_ADMIN_PANEL_PATH . 'components/component-top-bar.php'; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
Pero si component-top-bar.php es solo HTML sin lógica, entonces:
|
||||
- Opción 1: Eliminar component-top-bar.php (usar solo main.php)
|
||||
- Opción 2: Convertir component-top-bar.php en plantilla reutilizable
|
||||
|
||||
---
|
||||
|
||||
### 5. **header.php con Fallback** (Front-end no debería definir defaults)
|
||||
|
||||
**Código actual (header.php:34):**
|
||||
```php
|
||||
$top_bar_config = wp_parse_args($config, array(
|
||||
'message_text' => 'Accede a más de 200,000...',
|
||||
// ...
|
||||
));
|
||||
```
|
||||
|
||||
**Problema:** El front-end NO debería definir defaults.
|
||||
|
||||
**¿Por qué está esto aquí?**
|
||||
Probablemente por si Settings Manager falla o no retorna datos.
|
||||
|
||||
**Solución correcta:**
|
||||
```php
|
||||
// ANTES:
|
||||
$settings_manager = new APUS_Settings_Manager();
|
||||
$settings = $settings_manager->get_settings();
|
||||
$config = isset($settings['components']['top_bar']) ? $settings['components']['top_bar'] : array();
|
||||
$top_bar_config = wp_parse_args($config, array( /* defaults hardcodeados */ ));
|
||||
|
||||
// DESPUÉS:
|
||||
$settings_manager = new APUS_Settings_Manager();
|
||||
$settings = $settings_manager->get_settings(); // ← Ya incluye defaults merged
|
||||
$top_bar_config = $settings['components']['top_bar']; // ← Sin fallback necesario
|
||||
```
|
||||
|
||||
**Razón:** `get_settings()` del Settings Manager YA hace merge con defaults.
|
||||
|
||||
---
|
||||
|
||||
## 💡 SOLUCIÓN PROPUESTA
|
||||
|
||||
### PASO 1: Única Fuente de Verdad (Settings Manager)
|
||||
|
||||
**Crear constantes para valores reutilizables:**
|
||||
|
||||
```php
|
||||
// class-settings-manager.php
|
||||
|
||||
class APUS_Settings_Manager {
|
||||
|
||||
// Constantes de defaults
|
||||
const DEFAULT_TOPBAR_MESSAGE = 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.';
|
||||
const DEFAULT_TOPBAR_HIGHLIGHT = 'Nuevo:';
|
||||
const DEFAULT_TOPBAR_LINK_TEXT = 'Ver Catálogo';
|
||||
// ...
|
||||
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
'version' => APUS_ADMIN_PANEL_VERSION,
|
||||
'components' => array(
|
||||
'top_bar' => array(
|
||||
'enabled' => true,
|
||||
'message_text' => self::DEFAULT_TOPBAR_MESSAGE,
|
||||
'highlight_text' => self::DEFAULT_TOPBAR_HIGHLIGHT,
|
||||
'link_text' => self::DEFAULT_TOPBAR_LINK_TEXT,
|
||||
// ...
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Ventajas:**
|
||||
- ✅ Constantes documentadas en un solo lugar
|
||||
- ✅ Fácil de cambiar (1 línea en vez de 7 archivos)
|
||||
- ✅ PHP autocomplete para IDEs
|
||||
|
||||
---
|
||||
|
||||
### PASO 2: Eliminar Duplicaciones
|
||||
|
||||
#### 2.1. Sanitizer NO debe tener `get_defaults()`
|
||||
|
||||
```php
|
||||
// class-topbar-sanitizer.php
|
||||
|
||||
class APUS_TopBar_Sanitizer {
|
||||
|
||||
// ❌ ELIMINAR:
|
||||
// public function get_defaults() { ... }
|
||||
|
||||
// ✅ MANTENER SOLO:
|
||||
public function sanitize($data) {
|
||||
// Lógica de sanitización
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Si el Sanitizer necesita defaults para validación:**
|
||||
```php
|
||||
class APUS_TopBar_Sanitizer {
|
||||
|
||||
private $settings_manager;
|
||||
|
||||
public function __construct() {
|
||||
$this->settings_manager = new APUS_Settings_Manager();
|
||||
}
|
||||
|
||||
public function sanitize($data) {
|
||||
$defaults = $this->settings_manager->get_defaults()['components']['top_bar'];
|
||||
// Usar $defaults si es necesario para validación
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.2. JavaScript SIN Fallbacks Hardcodeados
|
||||
|
||||
```javascript
|
||||
// admin-app.js
|
||||
|
||||
renderTopBar(topBar) {
|
||||
// ANTES:
|
||||
// document.getElementById('topBarMessageText').value = topBar.message_text || 'Accede...';
|
||||
|
||||
// DESPUÉS:
|
||||
document.getElementById('topBarMessageText').value = topBar.message_text;
|
||||
document.getElementById('topBarHighlightText').value = topBar.highlight_text;
|
||||
document.getElementById('topBarLinkText').value = topBar.link_text;
|
||||
// ↑ Settings Manager YA hizo merge con defaults
|
||||
}
|
||||
```
|
||||
|
||||
**Razón:** AJAX obtiene settings de `get_settings()` que ya incluye defaults.
|
||||
|
||||
---
|
||||
|
||||
#### 2.3. HTML Dinámico (usar PHP)
|
||||
|
||||
```php
|
||||
<!-- main.php -->
|
||||
|
||||
<?php
|
||||
$settings_manager = new APUS_Settings_Manager();
|
||||
$defaults = $settings_manager->get_defaults()['components']['top_bar'];
|
||||
?>
|
||||
|
||||
<!-- Mensaje de texto -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-chat-text me-2"></i>Mensaje Principal
|
||||
</label>
|
||||
<textarea
|
||||
id="topBarMessageText"
|
||||
class="form-control"
|
||||
rows="2"
|
||||
maxlength="250"
|
||||
placeholder="Ej: <?php echo esc_attr($defaults['message_text']); ?>"
|
||||
><?php echo esc_html($defaults['message_text']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div id="topBarPreview" class="preview-top-bar">
|
||||
<span><?php echo esc_html($defaults['message_text']); ?></span>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.4. Front-end SIN Fallbacks
|
||||
|
||||
```php
|
||||
// header.php
|
||||
|
||||
<?php
|
||||
$settings_manager = new APUS_Settings_Manager();
|
||||
$settings = $settings_manager->get_settings(); // ← Ya incluye defaults merged
|
||||
$top_bar_config = $settings['components']['top_bar'];
|
||||
|
||||
// NO hacer wp_parse_args con defaults hardcodeados
|
||||
// ❌ $top_bar_config = wp_parse_args($config, array('message_text' => '...'));
|
||||
?>
|
||||
|
||||
<!-- Renderizar Top Bar -->
|
||||
<?php if ($top_bar_config['enabled']): ?>
|
||||
<div class="top-notification-bar">
|
||||
<span><?php echo esc_html($top_bar_config['message_text']); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.5. Eliminar component-top-bar.php (¿Duplicado?)
|
||||
|
||||
**Investigar:**
|
||||
1. ¿Se usa `component-top-bar.php` en algún lugar?
|
||||
2. Si NO se usa, eliminarlo
|
||||
3. Si SÍ se usa, refactorizar para que main.php lo incluya
|
||||
|
||||
---
|
||||
|
||||
## 📊 RESUMEN DE CAMBIOS
|
||||
|
||||
| Archivo | Acción | Razón |
|
||||
|---------|--------|-------|
|
||||
| `class-settings-manager.php` | ✅ **Usar constantes para defaults** | Única fuente de verdad |
|
||||
| `class-topbar-sanitizer.php` | ❌ **Eliminar `get_defaults()`** | Sanitizer no debe definir defaults |
|
||||
| `admin-app.js` | ❌ **Eliminar fallbacks hardcodeados** | AJAX ya retorna defaults merged |
|
||||
| `main.php` | ✏️ **Usar PHP dinámico para defaults** | Leer de Settings Manager |
|
||||
| `component-top-bar.php` | 🔍 **Investigar si es duplicado** | Posible eliminación |
|
||||
| `header.php` | ❌ **Eliminar fallbacks hardcodeados** | get_settings() ya incluye defaults |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 BENEFICIOS DE LA SOLUCIÓN
|
||||
|
||||
### Antes (Actual)
|
||||
```
|
||||
Cambiar "Accede a más de 200,000..." requiere:
|
||||
├── ✏️ Editar admin-app.js
|
||||
├── ✏️ Editar class-topbar-sanitizer.php
|
||||
├── ✏️ Editar class-settings-manager.php
|
||||
├── ✏️ Editar main.php (3 lugares)
|
||||
├── ✏️ Editar component-top-bar.php (2 lugares)
|
||||
└── ✏️ Editar header.php
|
||||
|
||||
Total: 7 archivos, ~10 líneas a cambiar
|
||||
Riesgo: 🔴 ALTO (fácil olvidar un archivo)
|
||||
```
|
||||
|
||||
### Después (Propuesto)
|
||||
```
|
||||
Cambiar "Accede a más de 200,000..." requiere:
|
||||
└── ✏️ Editar class-settings-manager.php (1 constante)
|
||||
|
||||
Total: 1 archivo, 1 línea
|
||||
Riesgo: 🟢 BAJO (cambio centralizado)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 IMPACTO EN OTROS COMPONENTES
|
||||
|
||||
**⚠️ IMPORTANTE:** Este problema probablemente se repite en los otros 3 componentes:
|
||||
|
||||
1. **Navbar** - ¿Tiene duplicación similar?
|
||||
2. **Let's Talk Button** - ¿Tiene duplicación similar?
|
||||
3. **Hero Section** - ¿Tiene duplicación similar?
|
||||
|
||||
**Recomendación:** Aplicar la misma refactorización a TODOS los componentes.
|
||||
|
||||
---
|
||||
|
||||
## ✅ CHECKLIST DE IMPLEMENTACIÓN
|
||||
|
||||
- [ ] Crear constantes en Settings Manager
|
||||
- [ ] Eliminar `get_defaults()` de TopBar Sanitizer
|
||||
- [ ] Eliminar fallbacks de admin-app.js
|
||||
- [ ] Convertir HTML de main.php a dinámico
|
||||
- [ ] Investigar si component-top-bar.php es necesario
|
||||
- [ ] Eliminar fallbacks de header.php
|
||||
- [ ] Verificar que NO hay regresiones
|
||||
- [ ] Aplicar solución a Navbar
|
||||
- [ ] Aplicar solución a Let's Talk Button
|
||||
- [ ] Aplicar solución a Hero Section
|
||||
|
||||
---
|
||||
|
||||
## 🔗 REFERENCIAS
|
||||
|
||||
- **Principio DRY:** Don't Repeat Yourself
|
||||
- **Single Source of Truth:** Una única fuente de verdad para datos
|
||||
- **Separation of Concerns:** Cada clase tiene una responsabilidad clara
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2025-01-13
|
||||
@@ -1,784 +0,0 @@
|
||||
# PLAN DE ACCIÓN: CORRECCIÓN DE DEFAULTS HARDCODEADOS
|
||||
|
||||
**Fecha inicio:** _[Pendiente]_
|
||||
**Fecha fin:** _[Pendiente]_
|
||||
**Estado:** 🔴 NO INICIADO
|
||||
|
||||
---
|
||||
|
||||
## 📋 OBJETIVO
|
||||
|
||||
Eliminar defaults hardcodeados del código y establecer tabla `wp_apus_theme_components_defaults` como única fuente de verdad.
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ TIEMPO ESTIMADO TOTAL
|
||||
|
||||
- **FASE 1:** 2-3 horas (Limpiar código actual)
|
||||
- **FASE 2:** 1 hora (Crear tabla defaults)
|
||||
- **FASE 3:** 3-4 horas (Corregir algoritmo)
|
||||
- **TOTAL:** 6-8 horas
|
||||
|
||||
---
|
||||
|
||||
## 🔄 ESTADO DEL PLAN
|
||||
|
||||
```
|
||||
FASE 1: Limpiar Código Actual [ ] 0/15 pasos completados
|
||||
FASE 2: Crear Tabla Defaults [ ] 0/4 pasos completados
|
||||
FASE 3: Corregir Algoritmo [ ] 0/8 pasos completados
|
||||
```
|
||||
|
||||
**Progreso total:** 0/27 pasos (0%)
|
||||
|
||||
---
|
||||
|
||||
# FASE 1: LIMPIAR CÓDIGO ACTUAL
|
||||
|
||||
**Objetivo:** Eliminar código mal implementado antes de corregir algoritmo
|
||||
|
||||
**Duración estimada:** 2-3 horas
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.1: Backup de Código Actual
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Crear branch de backup: `git checkout -b backup-antes-limpieza`
|
||||
- [ ] Hacer commit de estado actual: `git commit -am "backup: estado antes de limpieza de defaults"`
|
||||
- [ ] Push del backup: `git push origin backup-antes-limpieza`
|
||||
- [ ] Volver a main: `git checkout main`
|
||||
- [ ] Crear branch de trabajo: `git checkout -b fix/limpiar-defaults-hardcodeados`
|
||||
|
||||
**Verificación:** Branch `backup-antes-limpieza` existe en GitHub
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.2: Listar Archivos a Eliminar del Admin Panel
|
||||
**Duración:** 10 min
|
||||
|
||||
- [ ] Ejecutar: `dir admin\assets\js\component-*.js 2>nul` (listar JS componentes)
|
||||
- [ ] Ejecutar: `dir admin\assets\css\component-*.css 2>nul` (listar CSS componentes)
|
||||
- [ ] Ejecutar: `dir admin\components\component-*.php 2>nul` (listar PHP componentes)
|
||||
- [ ] Ejecutar: `dir admin\includes\sanitizers\class-*-sanitizer.php 2>nul` (listar sanitizers)
|
||||
- [ ] Documentar lista de archivos encontrados abajo
|
||||
|
||||
**Archivos encontrados:**
|
||||
```
|
||||
JS:
|
||||
-
|
||||
|
||||
CSS:
|
||||
-
|
||||
|
||||
PHP Componentes:
|
||||
-
|
||||
|
||||
Sanitizers:
|
||||
-
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.3: Eliminar Archivos JS de Componentes
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Eliminar: `admin/assets/js/component-navbar.js` (si existe)
|
||||
- [ ] Eliminar: `admin/assets/js/component-topbar.js` (si existe)
|
||||
- [ ] Eliminar: `admin/assets/js/component-hero.js` (si existe)
|
||||
- [ ] Eliminar: otros archivos `component-*.js` listados arriba
|
||||
- [ ] Verificar que NO quedan archivos: `dir admin\assets\js\component-*.js 2>nul`
|
||||
|
||||
**Archivos eliminados:** _[Anotar aquí]_
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.4: Eliminar Archivos CSS de Componentes
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Eliminar: `admin/assets/css/component-navbar.css` (si existe)
|
||||
- [ ] Eliminar: `admin/assets/css/component-topbar.css` (si existe)
|
||||
- [ ] Eliminar: `admin/assets/css/component-hero.css` (si existe)
|
||||
- [ ] Eliminar: otros archivos `component-*.css` listados arriba
|
||||
- [ ] Verificar que NO quedan archivos: `dir admin\assets\css\component-*.css 2>nul`
|
||||
|
||||
**Archivos eliminados:** _[Anotar aquí]_
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.5: Eliminar Archivos PHP de Componentes
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Eliminar: `admin/components/component-navbar.php` (si existe)
|
||||
- [ ] Eliminar: `admin/components/component-top-bar.php` (si existe)
|
||||
- [ ] Eliminar: `admin/components/component-hero.php` (si existe)
|
||||
- [ ] Eliminar: otros archivos `component-*.php` listados arriba
|
||||
- [ ] Verificar que NO quedan archivos: `dir admin\components\component-*.php 2>nul`
|
||||
|
||||
**Archivos eliminados:** _[Anotar aquí]_
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.6: Eliminar Sanitizers de Componentes
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Eliminar: `admin/includes/sanitizers/class-topbar-sanitizer.php` (si existe)
|
||||
- [ ] Eliminar: `admin/includes/sanitizers/class-navbar-sanitizer.php` (si existe)
|
||||
- [ ] Eliminar: otros archivos `class-*-sanitizer.php` listados arriba
|
||||
- [ ] Verificar que NO quedan archivos: `dir admin\includes\sanitizers\class-*-sanitizer.php 2>nul`
|
||||
|
||||
**Archivos eliminados:** _[Anotar aquí]_
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.7: Limpiar class-admin-menu.php
|
||||
**Duración:** 10 min
|
||||
|
||||
**Archivo:** `admin/includes/class-admin-menu.php`
|
||||
|
||||
- [ ] Leer el archivo completo
|
||||
- [ ] Identificar líneas que encolaron CSS de componentes (wp_enqueue_style para component-*.css)
|
||||
- [ ] Identificar líneas que encolaron JS de componentes (wp_enqueue_script para component-*.js)
|
||||
- [ ] Eliminar todas las líneas encontradas
|
||||
- [ ] Verificar que método `enqueue_assets()` solo encola archivos del core (admin-panel.css, admin-app.js)
|
||||
|
||||
**Líneas eliminadas:** _[Anotar números de línea]_
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.8: Limpiar admin/pages/main.php (Parte 1: Analizar)
|
||||
**Duración:** 15 min
|
||||
|
||||
**Archivo:** `admin/pages/main.php`
|
||||
|
||||
- [ ] Leer el archivo completo
|
||||
- [ ] Buscar secciones de tabs de navegación (ej: Top Bar, Navbar, etc.)
|
||||
- [ ] Buscar secciones de tab-pane con formularios de componentes
|
||||
- [ ] Documentar números de línea a eliminar abajo
|
||||
|
||||
**Secciones encontradas:**
|
||||
```
|
||||
Tabs navegación:
|
||||
Líneas: _____ a _____
|
||||
|
||||
Tab-pane Top Bar:
|
||||
Líneas: _____ a _____
|
||||
|
||||
Tab-pane Navbar:
|
||||
Líneas: _____ a _____
|
||||
|
||||
Otros:
|
||||
Líneas: _____ a _____
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.9: Limpiar admin/pages/main.php (Parte 2: Eliminar)
|
||||
**Duración:** 10 min
|
||||
|
||||
**Archivo:** `admin/pages/main.php`
|
||||
|
||||
Usando los rangos de líneas identificados en PASO 1.8:
|
||||
|
||||
- [ ] Eliminar sección de tab navegación de componentes
|
||||
- [ ] Eliminar sección tab-pane de Top Bar
|
||||
- [ ] Eliminar sección tab-pane de Navbar
|
||||
- [ ] Eliminar otras secciones documentadas arriba
|
||||
- [ ] Verificar que NO quedan referencias a componentes
|
||||
- [ ] Dejar SOLO estructura base del admin panel
|
||||
|
||||
**Verificación:** Buscar "top_bar", "navbar", "component" en el archivo - NO debe encontrar nada
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.10: Limpiar admin/assets/js/admin-app.js
|
||||
**Duración:** 15 min
|
||||
|
||||
**Archivo:** `admin/assets/js/admin-app.js`
|
||||
|
||||
- [ ] Leer el archivo completo
|
||||
- [ ] Buscar métodos `renderTopBar()`, `renderNavbar()`, etc.
|
||||
- [ ] Buscar referencias a componentes en método `collectFormData()`
|
||||
- [ ] Buscar valores hardcodeados tipo: `'Accede a más de 200,000...'`
|
||||
- [ ] Eliminar todos los métodos y referencias encontradas
|
||||
- [ ] Verificar que NO quedan fallbacks hardcodeados (ej: `|| 'default value'`)
|
||||
|
||||
**Líneas eliminadas:** _[Anotar aquí]_
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.11: Limpiar class-settings-manager.php (Parte 1)
|
||||
**Duración:** 10 min
|
||||
|
||||
**Archivo:** `admin/includes/class-settings-manager.php`
|
||||
|
||||
- [ ] Leer método `get_defaults()` completo
|
||||
- [ ] Identificar sección de defaults de componentes (top_bar, navbar, etc.)
|
||||
- [ ] Documentar líneas a eliminar
|
||||
|
||||
**Defaults encontrados:**
|
||||
```
|
||||
top_bar: Líneas _____ a _____
|
||||
navbar: Líneas _____ a _____
|
||||
otros: Líneas _____ a _____
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.12: Limpiar class-settings-manager.php (Parte 2)
|
||||
**Duración:** 15 min
|
||||
|
||||
**Archivo:** `admin/includes/class-settings-manager.php`
|
||||
|
||||
- [ ] Eliminar método `get_defaults()` COMPLETO (se reemplazará después)
|
||||
- [ ] Leer método `sanitize_settings()`
|
||||
- [ ] Eliminar secciones de sanitización de componentes
|
||||
- [ ] Verificar que NO quedan referencias a top_bar, navbar, etc.
|
||||
|
||||
**Líneas eliminadas:** _[Anotar aquí]_
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.13: Limpiar Tema (header.php y otros)
|
||||
**Duración:** 20 min
|
||||
|
||||
- [ ] Leer `header.php` completo
|
||||
- [ ] Buscar código que lea de Settings Manager para componentes
|
||||
- [ ] Buscar valores hardcodeados duplicados (ej: "Accede a más de 200,000...")
|
||||
- [ ] Documentar qué encontraste
|
||||
|
||||
**Código encontrado en header.php:**
|
||||
```
|
||||
Líneas: _____ a _____
|
||||
Descripción: _______________
|
||||
```
|
||||
|
||||
- [ ] Revisar otros archivos del tema si es necesario
|
||||
- [ ] Documentar archivos revisados
|
||||
|
||||
**Archivos del tema revisados:**
|
||||
- [ ] header.php
|
||||
- [ ] footer.php
|
||||
- [ ] _______
|
||||
|
||||
**Decisión:** ¿Eliminar código configurable del tema o dejarlo?
|
||||
_[Decidir con usuario antes de eliminar]_
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.14: Limpiar Base de Datos
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Conectar a base de datos (phpMyAdmin o terminal)
|
||||
- [ ] Ejecutar: `SELECT * FROM wp_apus_theme_components;`
|
||||
- [ ] Documentar componentes encontrados:
|
||||
|
||||
**Componentes en DB:**
|
||||
```
|
||||
component_name: ___________
|
||||
component_name: ___________
|
||||
```
|
||||
|
||||
- [ ] Ejecutar: `DELETE FROM wp_apus_theme_components;` (vaciar tabla)
|
||||
- [ ] Verificar: `SELECT COUNT(*) FROM wp_apus_theme_components;` (debe ser 0)
|
||||
|
||||
**Registros eliminados:** _____
|
||||
|
||||
---
|
||||
|
||||
## PASO 1.15: Commit de Limpieza
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Ejecutar: `git status` (ver todos los cambios)
|
||||
- [ ] Ejecutar: `git add .`
|
||||
- [ ] Ejecutar commit:
|
||||
```bash
|
||||
git commit -m "fix: eliminar implementación incorrecta de componentes
|
||||
|
||||
- Eliminar archivos JS/CSS/PHP de componentes mal implementados
|
||||
- Limpiar class-admin-menu.php de encolamiento de componentes
|
||||
- Limpiar admin/pages/main.php de secciones de componentes
|
||||
- Limpiar admin-app.js de métodos y defaults hardcodeados
|
||||
- Limpiar class-settings-manager.php de get_defaults() y sanitizers
|
||||
- Vaciar tabla wp_apus_theme_components
|
||||
|
||||
Preparación para implementar arquitectura correcta con tabla defaults.
|
||||
|
||||
Ref: PROBLEMA-DEFAULTS-HARDCODEADOS-ALGORITMO.md"
|
||||
```
|
||||
- [ ] Ejecutar: `git push origin fix/limpiar-defaults-hardcodeados`
|
||||
|
||||
---
|
||||
|
||||
## ✅ CHECKLIST FASE 1 COMPLETA
|
||||
|
||||
- [ ] Backup creado en branch separado
|
||||
- [ ] Archivos de componentes eliminados (JS, CSS, PHP, Sanitizers)
|
||||
- [ ] class-admin-menu.php limpiado
|
||||
- [ ] admin/pages/main.php limpiado
|
||||
- [ ] admin-app.js limpiado
|
||||
- [ ] class-settings-manager.php limpiado
|
||||
- [ ] Tema revisado
|
||||
- [ ] Base de datos vaciada
|
||||
- [ ] Commit y push realizados
|
||||
|
||||
**Estado FASE 1:** ⬜ Pendiente | 🟡 En progreso | ✅ Completada
|
||||
|
||||
---
|
||||
|
||||
# FASE 2: CREAR TABLA DE DEFAULTS
|
||||
|
||||
**Objetivo:** Implementar tabla `wp_apus_theme_components_defaults` en base de datos
|
||||
|
||||
**Duración estimada:** 1 hora
|
||||
|
||||
---
|
||||
|
||||
## PASO 2.1: Crear Script SQL
|
||||
**Duración:** 10 min
|
||||
|
||||
- [ ] Crear archivo: `admin/includes/migrations/create-defaults-table.sql`
|
||||
- [ ] Copiar SQL de `PROBLEMA-DEFAULTS-HARDCODEADOS-ALGORITMO.md` (líneas 418-437)
|
||||
- [ ] Verificar sintaxis SQL
|
||||
|
||||
**Contenido del archivo:**
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS wp_apus_theme_components_defaults (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
component_name VARCHAR(50) NOT NULL COMMENT 'Nombre del componente',
|
||||
config_key VARCHAR(100) NOT NULL COMMENT 'Clave de configuración',
|
||||
config_value TEXT NOT NULL COMMENT 'Valor por defecto extraído del tema',
|
||||
data_type ENUM('string','integer','boolean','array','json') NOT NULL,
|
||||
version VARCHAR(20) DEFAULT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_default_config (component_name, config_key),
|
||||
INDEX idx_component_name (component_name),
|
||||
INDEX idx_config_key (config_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 2.2: Ejecutar SQL en Base de Datos
|
||||
**Duración:** 5 min
|
||||
|
||||
**Método 1: phpMyAdmin**
|
||||
- [ ] Abrir phpMyAdmin
|
||||
- [ ] Seleccionar base de datos del tema
|
||||
- [ ] Ir a pestaña SQL
|
||||
- [ ] Copiar contenido de `create-defaults-table.sql`
|
||||
- [ ] Ejecutar SQL
|
||||
|
||||
**Método 2: Terminal/CMD**
|
||||
- [ ] Conectar a MySQL/MariaDB
|
||||
- [ ] Ejecutar: `USE nombre_base_datos;`
|
||||
- [ ] Copiar y ejecutar SQL
|
||||
|
||||
**Verificación:**
|
||||
- [ ] Ejecutar: `SHOW TABLES LIKE 'wp_apus_theme_components_defaults';`
|
||||
- [ ] Debe retornar la tabla
|
||||
|
||||
---
|
||||
|
||||
## PASO 2.3: Verificar Estructura de Tabla
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Ejecutar: `DESCRIBE wp_apus_theme_components_defaults;`
|
||||
- [ ] Verificar columnas:
|
||||
- [ ] id (BIGINT)
|
||||
- [ ] component_name (VARCHAR 50)
|
||||
- [ ] config_key (VARCHAR 100)
|
||||
- [ ] config_value (TEXT)
|
||||
- [ ] data_type (ENUM)
|
||||
- [ ] version (VARCHAR 20)
|
||||
- [ ] created_at (DATETIME)
|
||||
- [ ] updated_at (DATETIME)
|
||||
- [ ] Verificar índices:
|
||||
- [ ] PRIMARY KEY (id)
|
||||
- [ ] UNIQUE (component_name, config_key)
|
||||
- [ ] INDEX (component_name)
|
||||
- [ ] INDEX (config_key)
|
||||
|
||||
---
|
||||
|
||||
## PASO 2.4: Commit de Creación de Tabla
|
||||
**Duración:** 5 min
|
||||
|
||||
- [ ] Ejecutar: `git add admin/includes/migrations/create-defaults-table.sql`
|
||||
- [ ] Ejecutar commit:
|
||||
```bash
|
||||
git commit -m "feat(db): crear tabla wp_apus_theme_components_defaults
|
||||
|
||||
- Tabla para almacenar valores por defecto de componentes
|
||||
- Estructura normalizada (un row por campo)
|
||||
- Índices para optimizar búsquedas
|
||||
- Script SQL reutilizable en create-defaults-table.sql
|
||||
|
||||
Ref: PROBLEMA-DEFAULTS-HARDCODEADOS-ALGORITMO.md"
|
||||
```
|
||||
- [ ] Ejecutar: `git push origin fix/limpiar-defaults-hardcodeados`
|
||||
|
||||
---
|
||||
|
||||
## ✅ CHECKLIST FASE 2 COMPLETA
|
||||
|
||||
- [ ] Script SQL creado en `admin/includes/migrations/create-defaults-table.sql`
|
||||
- [ ] SQL ejecutado en base de datos
|
||||
- [ ] Tabla `wp_apus_theme_components_defaults` existe
|
||||
- [ ] Estructura verificada (8 columnas, 3 índices)
|
||||
- [ ] Commit y push realizados
|
||||
|
||||
**Estado FASE 2:** ⬜ Pendiente | 🟡 En progreso | ✅ Completada
|
||||
|
||||
---
|
||||
|
||||
# FASE 3: CORREGIR ALGORITMO
|
||||
|
||||
**Objetivo:** Modificar archivos del algoritmo para usar tabla defaults en lugar de hardcodear valores
|
||||
|
||||
**Duración estimada:** 3-4 horas
|
||||
|
||||
---
|
||||
|
||||
## PASO 3.1: Modificar PASO 12 del Algoritmo (Parte 1: Analizar)
|
||||
**Duración:** 15 min
|
||||
|
||||
**Archivo:** `_planeacion/apus-theme/admin-panel-theme/100-modularizacion-admin/00-algoritmo/12-F03-IMPLEMENTACION-IMPLEMENTAR-ADMIN-JS.md`
|
||||
|
||||
- [ ] Leer archivo completo
|
||||
- [ ] Identificar líneas con objeto `DEFAULT_CONFIG` (aprox líneas 43-51, 169-177)
|
||||
- [ ] Identificar líneas con fallbacks en método `render()` (aprox líneas 117-129)
|
||||
- [ ] Identificar líneas con botón reset (aprox líneas 196-204)
|
||||
- [ ] Documentar cambios necesarios
|
||||
|
||||
**Líneas a modificar:**
|
||||
```
|
||||
DEFAULT_CONFIG: Líneas _____ a _____
|
||||
Fallbacks render(): Líneas _____ a _____
|
||||
Botón reset: Líneas _____ a _____
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 3.2: Modificar PASO 12 del Algoritmo (Parte 2: Eliminar DEFAULT_CONFIG)
|
||||
**Duración:** 20 min
|
||||
|
||||
**Archivo:** `12-F03-IMPLEMENTACION-IMPLEMENTAR-ADMIN-JS.md`
|
||||
|
||||
- [ ] Eliminar sección que instruye crear objeto `DEFAULT_CONFIG`
|
||||
- [ ] Eliminar ejemplo de código con `const DEFAULT_CONFIG = {...}`
|
||||
- [ ] Agregar nota: "❌ NO crear objeto DEFAULT_CONFIG - Los defaults vienen de DB vía AJAX"
|
||||
|
||||
**Texto a agregar:**
|
||||
```markdown
|
||||
## ❌ IMPORTANTE: NO Crear Objeto DEFAULT_CONFIG
|
||||
|
||||
**PROHIBIDO crear objeto con defaults hardcodeados en JavaScript.**
|
||||
|
||||
Los valores por defecto vienen de la base de datos vía AJAX.
|
||||
Settings Manager lee de tabla `wp_apus_theme_components_defaults`.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 3.3: Modificar PASO 12 del Algoritmo (Parte 3: Corregir Fallbacks)
|
||||
**Duración:** 20 min
|
||||
|
||||
**Archivo:** `12-F03-IMPLEMENTACION-IMPLEMENTAR-ADMIN-JS.md`
|
||||
|
||||
- [ ] Modificar sección del método `render()`
|
||||
- [ ] Eliminar ejemplos con fallbacks: `config.field || 'default value'`
|
||||
- [ ] Reemplazar por: `config.field` (sin fallback)
|
||||
- [ ] Agregar nota explicando que AJAX SIEMPRE retorna datos completos (DB + defaults merged)
|
||||
|
||||
**Ejemplo ANTES (INCORRECTO):**
|
||||
```javascript
|
||||
bgColorInput.value = config.custom_styles?.bg_color || '#000000';
|
||||
```
|
||||
|
||||
**Ejemplo DESPUÉS (CORRECTO):**
|
||||
```javascript
|
||||
bgColorInput.value = config.custom_styles?.bg_color;
|
||||
// NO fallback necesario - Settings Manager ya hace merge con defaults de DB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 3.4: Modificar PASO 12 del Algoritmo (Parte 4: Botón Reset)
|
||||
**Duración:** 15 min
|
||||
|
||||
**Archivo:** `12-F03-IMPLEMENTACION-IMPLEMENTAR-ADMIN-JS.md`
|
||||
|
||||
- [ ] Modificar sección del botón "Reset to Defaults"
|
||||
- [ ] Cambiar de `loadConfig(DEFAULT_CONFIG)` a llamada AJAX
|
||||
- [ ] Agregar código para llamar endpoint que retorna defaults de DB
|
||||
|
||||
**Código a agregar:**
|
||||
```javascript
|
||||
// Botón Reset to Defaults
|
||||
resetBtn.addEventListener('click', function() {
|
||||
if (confirm('¿Restaurar valores por defecto?')) {
|
||||
// Llamar AJAX para obtener defaults de DB
|
||||
axios.get(apusAdminData.ajaxUrl, {
|
||||
params: {
|
||||
action: 'get_component_defaults',
|
||||
component: 'component_name',
|
||||
nonce: apusAdminData.nonce
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
loadConfig(response.data);
|
||||
// Guardar defaults como config personalizada
|
||||
saveForm();
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 3.5: Crear NUEVO PASO en Algoritmo (Poblar Defaults)
|
||||
**Duración:** 30 min
|
||||
|
||||
- [ ] Crear archivo: `_planeacion/.../00-algoritmo/07B-F02-DISENO-POBLAR-DEFAULTS-DB.md`
|
||||
- [ ] Ubicación: DESPUÉS de PASO 7, ANTES de PASO 8
|
||||
|
||||
**Contenido del archivo:**
|
||||
```markdown
|
||||
# PASO 7B: POBLAR TABLA DE DEFAULTS
|
||||
|
||||
**Prerequisito:** PASO 7 completado (código configurable documentado)
|
||||
|
||||
## Objetivo
|
||||
|
||||
Insertar valores por defecto del componente en tabla `wp_apus_theme_components_defaults`.
|
||||
|
||||
## 7B.1 Leer Valores Extraídos
|
||||
|
||||
- Abrir archivo del PASO 6: `03-DOCUMENTACION-ESTRUCTURA-DATOS.md`
|
||||
- Identificar TODOS los campos con sus valores por defecto
|
||||
- Valores de textos/URLs: Del código hardcodeado actual
|
||||
- Valores de colores/estilos: Del CSS original del componente
|
||||
|
||||
## 7B.2 Generar Script SQL
|
||||
|
||||
Crear archivo: `[componente]/defaults-insert.sql`
|
||||
|
||||
Formato:
|
||||
INSERT INTO wp_apus_theme_components_defaults
|
||||
(component_name, config_key, config_value, data_type, version)
|
||||
VALUES
|
||||
('[component_name]', 'enabled', '1', 'boolean', '2.1.4'),
|
||||
('[component_name]', '[field1]', '[valor]', 'string', '2.1.4'),
|
||||
...
|
||||
|
||||
## 7B.3 Ejecutar SQL
|
||||
|
||||
- Conectar a base de datos
|
||||
- Ejecutar script SQL
|
||||
- Verificar: SELECT * FROM wp_apus_theme_components_defaults WHERE component_name='[nombre]';
|
||||
|
||||
## 7B.4 Verificar
|
||||
|
||||
- [ ] Todos los campos del PASO 6 tienen row en tabla defaults
|
||||
- [ ] Valores coinciden con los extraídos del código/CSS actual
|
||||
- [ ] data_type es correcto para cada campo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 3.6: Modificar PASO 14 del Algoritmo (Eliminar get_defaults)
|
||||
**Duración:** 30 min
|
||||
|
||||
**Archivo:** `_planeacion/.../00-algoritmo/14-F04-CIERRE-GIT-COMMITS.md`
|
||||
|
||||
- [ ] Leer sección "14.4 Modificar Settings Manager (CRÍTICO)"
|
||||
- [ ] Leer subsección "Modificación 1: Agregar Defaults (línea ~146)"
|
||||
- [ ] Eliminar TODO el ejemplo del método `get_defaults()` con array hardcodeado (líneas ~88-123)
|
||||
- [ ] Reemplazar por instrucciones para leer de tabla defaults
|
||||
|
||||
**Texto a eliminar:**
|
||||
```php
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
'version' => APUS_ADMIN_PANEL_VERSION,
|
||||
'components' => array(
|
||||
'component_name' => array(
|
||||
'enabled' => true,
|
||||
// ... defaults hardcodeados
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Texto a agregar:**
|
||||
```markdown
|
||||
### Modificación: Settings Manager Lee de Tabla Defaults
|
||||
|
||||
**❌ NO crear método get_defaults() con array hardcodeado**
|
||||
|
||||
Los defaults ya están en tabla `wp_apus_theme_components_defaults` (insertados en PASO 7B).
|
||||
|
||||
Settings Manager debe leer de DB, NO tener defaults hardcodeados.
|
||||
|
||||
Ver método `get_component_config()` que hace merge automático:
|
||||
1. Lee config personalizada de `wp_apus_theme_components`
|
||||
2. Si no existe → Lee defaults de `wp_apus_theme_components_defaults`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 3.7: Modificar DB Manager (Agregar get_component_defaults)
|
||||
**Duración:** 30 min
|
||||
|
||||
**Archivo:** `admin/includes/class-db-manager.php`
|
||||
|
||||
- [ ] Leer archivo completo
|
||||
- [ ] Buscar método `get_component($component_name)`
|
||||
- [ ] Copiar método y modificar para leer de tabla `_defaults`
|
||||
- [ ] Agregar nuevo método
|
||||
|
||||
**Código a agregar:**
|
||||
```php
|
||||
/**
|
||||
* Get component default values from defaults table
|
||||
*
|
||||
* @param string $component_name
|
||||
* @return array
|
||||
*/
|
||||
public function get_component_defaults($component_name) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'apus_theme_components_defaults';
|
||||
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT config_key, config_value, data_type
|
||||
FROM $table_name
|
||||
WHERE component_name = %s",
|
||||
$component_name
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if (empty($results)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Convertir rows a array asociativo
|
||||
$config = array();
|
||||
foreach ($results as $row) {
|
||||
$config[$row['config_key']] = $this->cast_value(
|
||||
$row['config_value'],
|
||||
$row['data_type']
|
||||
);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast value to correct type based on data_type
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param string $type
|
||||
* @return mixed
|
||||
*/
|
||||
private function cast_value($value, $type) {
|
||||
switch ($type) {
|
||||
case 'boolean':
|
||||
return (bool) $value;
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
case 'array':
|
||||
case 'json':
|
||||
return json_decode($value, true);
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PASO 3.8: Modificar Settings Manager (get_component_config)
|
||||
**Duración:** 20 min
|
||||
|
||||
**Archivo:** `admin/includes/class-settings-manager.php`
|
||||
|
||||
- [ ] Buscar método `get_component_config($component_name)`
|
||||
- [ ] Modificar para leer de tabla defaults si no hay config personalizada
|
||||
|
||||
**Código ANTES:**
|
||||
```php
|
||||
public function get_component_config($component_name) {
|
||||
$settings = $this->get_settings();
|
||||
$defaults = $this->get_defaults(); // ← Método hardcodeado
|
||||
|
||||
return wp_parse_args(
|
||||
$settings['components'][$component_name] ?? array(),
|
||||
$defaults['components'][$component_name] ?? array()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Código DESPUÉS:**
|
||||
```php
|
||||
public function get_component_config($component_name) {
|
||||
// 1. Intentar leer config personalizada
|
||||
$user_config = $this->db_manager->get_component($component_name);
|
||||
|
||||
if (!empty($user_config)) {
|
||||
return $user_config; // Usuario ya personalizó
|
||||
}
|
||||
|
||||
// 2. Si no hay personalización, leer defaults de tabla
|
||||
$defaults = $this->db_manager->get_component_defaults($component_name);
|
||||
|
||||
if (!empty($defaults)) {
|
||||
return $defaults; // Usar defaults de DB
|
||||
}
|
||||
|
||||
// 3. Error: componente sin defaults
|
||||
error_log("APUS Theme: No defaults found for component: {$component_name}");
|
||||
return array();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ CHECKLIST FASE 3 COMPLETA
|
||||
|
||||
- [ ] PASO 12 modificado (eliminado DEFAULT_CONFIG y fallbacks)
|
||||
- [ ] PASO 7B creado (poblar defaults en DB)
|
||||
- [ ] PASO 14 modificado (eliminado get_defaults hardcodeado)
|
||||
- [ ] DB Manager modificado (agregado get_component_defaults)
|
||||
- [ ] Settings Manager modificado (lee de tabla defaults)
|
||||
- [ ] Todos los cambios commiteados
|
||||
|
||||
**Estado FASE 3:** ⬜ Pendiente | 🟡 En progreso | ✅ Completada
|
||||
|
||||
---
|
||||
|
||||
## 🎯 RESUMEN FINAL
|
||||
|
||||
Una vez completadas las 3 fases:
|
||||
|
||||
### ✅ Lo que se logró:
|
||||
1. Código actual limpiado (sin implementaciones incorrectas)
|
||||
2. Tabla `wp_apus_theme_components_defaults` creada y funcionando
|
||||
3. Algoritmo corregido (sin defaults hardcodeados en JS/PHP)
|
||||
4. DB Manager y Settings Manager leen de tabla defaults
|
||||
|
||||
### 🚀 Próximos pasos:
|
||||
1. Ejecutar algoritmo CORREGIDO para primer componente (ej: Navbar)
|
||||
2. Pasos 1-13: Generar documentación
|
||||
3. PASO 7B: Insertar defaults en DB
|
||||
4. PASO 14: Implementar código real
|
||||
5. PASO 15-16: Testing y cierre
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** _[Fecha]_
|
||||
**Estado general:** ⬜ Pendiente | 🟡 En progreso | ✅ Completado
|
||||
@@ -1,670 +0,0 @@
|
||||
# PROBLEMA: Defaults Hardcodeados en Algoritmo de Modularización
|
||||
|
||||
**Fecha:** 2025-01-13
|
||||
**Estado:** 🔴 EN INVESTIGACIÓN
|
||||
**Prioridad:** ALTA
|
||||
|
||||
---
|
||||
|
||||
## 📋 CONTEXTO
|
||||
|
||||
### Situación Actual
|
||||
|
||||
El tema WordPress tiene valores hardcodeados en múltiples archivos:
|
||||
```
|
||||
wp-content/themes/apus-theme/
|
||||
├── *.php → Valores hardcodeados
|
||||
├── *.html → Valores hardcodeados
|
||||
├── assets/
|
||||
├── css/ → Valores hardcodeados
|
||||
└── js/ → Valores hardcodeados
|
||||
```
|
||||
|
||||
### Objetivo del Sistema
|
||||
|
||||
El **Admin Panel** debe permitir personalizar la mayoría de valores que actualmente están hardcodeados.
|
||||
|
||||
### Sistema de Persistencia Disponible
|
||||
|
||||
**✅ Ya existe tabla personalizada:** `wp_apus_theme_components`
|
||||
|
||||
**Ubicación:** Base de datos WordPress
|
||||
**Documentación:** Ver `ANALISIS-ESTRUCTURA-ADMIN.md`
|
||||
|
||||
**Estructura:**
|
||||
```sql
|
||||
CREATE TABLE wp_apus_theme_components (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
component_name VARCHAR(50) NOT NULL, -- 'topbar', 'navbar', 'hero', etc.
|
||||
config_key VARCHAR(100) NOT NULL, -- 'message_text', 'bg_color', etc.
|
||||
config_value TEXT NOT NULL, -- Valor del campo
|
||||
data_type ENUM('string','integer','boolean','array','json'),
|
||||
version VARCHAR(20),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_config (component_name, config_key)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔴 PROBLEMA IDENTIFICADO
|
||||
|
||||
### Descripción
|
||||
|
||||
El algoritmo de modularización ubicado en:
|
||||
```
|
||||
_planeacion/apus-theme/admin-panel-theme/100-modularizacion-admin/00-algoritmo/
|
||||
```
|
||||
|
||||
**❌ PROBLEMA CRÍTICO ENCONTRADO EN PASO 12:**
|
||||
- El algoritmo instruye crear un objeto `DEFAULT_CONFIG` en JavaScript con TODOS los valores por defecto hardcodeados
|
||||
- Esto viola el principio de Single Source of Truth
|
||||
- Los defaults deberían venir de PHP (Settings Manager) vía AJAX, NO estar duplicados en JavaScript
|
||||
|
||||
### Evidencia del Problema
|
||||
|
||||
**Ubicación:** `00-algoritmo/12-F03-IMPLEMENTACION-IMPLEMENTAR-ADMIN-JS.md`
|
||||
|
||||
**Líneas 43-51 y 169-177:**
|
||||
```javascript
|
||||
const DEFAULT_CONFIG = {
|
||||
enabled: true,
|
||||
campo1: 'valor default',
|
||||
custom_styles: {
|
||||
background_color: '#0E2337',
|
||||
// ... todos los campos del PASO 6
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Líneas 117-129 (método render()):**
|
||||
```javascript
|
||||
document.getElementById('topBarIconClass').value = config.icon_class || '';
|
||||
document.getElementById('topBarShowLink').checked = config.show_link || false;
|
||||
const bgColorInput = document.getElementById('topBarBgColor');
|
||||
bgColorInput.value = config.custom_styles?.bg_color || '#000000'; // ← Fallback hardcodeado
|
||||
```
|
||||
|
||||
### Por Qué es un Problema
|
||||
|
||||
1. **Duplicación de defaults:**
|
||||
- PHP Settings Manager tiene defaults
|
||||
- JavaScript TAMBIÉN tiene defaults (duplicado)
|
||||
- Tabla DB puede tener defaults (triplicado si se hace seed)
|
||||
|
||||
2. **Violación de Single Source of Truth:**
|
||||
- Cambiar un default requiere editar JavaScript Y PHP
|
||||
- Alto riesgo de inconsistencias
|
||||
|
||||
3. **Arquitectura incorrecta:**
|
||||
- JavaScript NO debería tener fallbacks porque `get_settings()` de PHP ya hace merge con defaults
|
||||
- AJAX siempre retorna datos completos (DB + defaults merged)
|
||||
|
||||
---
|
||||
|
||||
## ❓ PREGUNTAS PARA INVESTIGACIÓN
|
||||
|
||||
### PREGUNTA 1: Ubicación del Problema ✅ RESPONDIDA
|
||||
**¿En cuál(es) paso(s) del algoritmo se guardan valores en archivos JS?**
|
||||
|
||||
- [ ] PASO 1: Crear issue
|
||||
- [ ] PASO 2: Análisis con Serena
|
||||
- [ ] PASO 3: Crear estructura de documentación
|
||||
- [ ] PASO 4: Documentar código real
|
||||
- [ ] PASO 5: Documentar campos configurables
|
||||
- [ ] PASO 6: Estructura JSON
|
||||
- [ ] PASO 7: Documentar código configurable
|
||||
- [ ] PASO 8: Referencia AJAX
|
||||
- [ ] PASO 9: Plantilla estructura HTML
|
||||
- [ ] PASO 10: Ejemplos componentes
|
||||
- [ ] PASO 11: Ensamblar admin HTML
|
||||
- [X] **PASO 12: Implementar admin JS** ← ❌ AQUÍ ESTÁ EL PROBLEMA
|
||||
- [ ] PASO 13: CSS admin panel
|
||||
- [ ] PASO 14: Git commits
|
||||
- [ ] PASO 15: Testing
|
||||
- [ ] PASO 16: Cerrar issue
|
||||
|
||||
**✅ Respuesta encontrada:**
|
||||
- **PASO 12** instruye crear objeto `DEFAULT_CONFIG` en JavaScript con todos los defaults hardcodeados
|
||||
- **Líneas problemáticas:** 43-51, 169-177, 117-129, 223-229
|
||||
- **Archivos afectados:** `component-[nombre].js` (uno por cada componente)
|
||||
|
||||
---
|
||||
|
||||
### PREGUNTA 2: Archivos JS Afectados ✅ RESPONDIDA
|
||||
**¿Qué archivos JavaScript están siendo modificados con valores hardcodeados?**
|
||||
|
||||
Opciones probables:
|
||||
- [X] `admin/assets/js/admin-app.js` ← Fallbacks en método `render()`
|
||||
- [X] `admin/assets/js/component-navbar.js` ← Si se siguió PASO 12
|
||||
- [X] `admin/assets/js/component-*.js` (otros componentes) ← Si se siguió PASO 12
|
||||
- [ ] Archivos JS del tema (fuera de admin)
|
||||
- [ ] Otro: _______________
|
||||
|
||||
**✅ Respuesta encontrada:**
|
||||
- **Patrón del algoritmo:** CADA componente debe tener su propio archivo `component-[nombre].js`
|
||||
- **Cada archivo debe tener:** Objeto `DEFAULT_CONFIG` con todos los defaults
|
||||
- **Ubicación:** `admin/assets/js/component-*.js`
|
||||
- **Comprobación en código actual:** `admin-app.js:357` tiene fallback hardcodeado para Top Bar
|
||||
|
||||
---
|
||||
|
||||
### PREGUNTA 3: Tipo de Valores ✅ RESPONDIDA
|
||||
**¿Qué tipo de valores por defecto se están guardando en JS?**
|
||||
|
||||
Opciones:
|
||||
- [X] Textos (ej: "Accede a más de 200,000...")
|
||||
- [X] URLs (ej: "/catalogo")
|
||||
- [X] Colores (ej: "#0E2337")
|
||||
- [X] Iconos (ej: "bi bi-megaphone-fill")
|
||||
- [X] Configuraciones booleanas (ej: enabled: true)
|
||||
- [X] **Todos los anteriores** ← CORRECTO
|
||||
- [ ] Otro: _______________
|
||||
|
||||
**✅ Respuesta encontrada:**
|
||||
- Según PASO 12 líneas 169-177, el objeto `DEFAULT_CONFIG` debe contener **TODOS** los campos del PASO 6
|
||||
- Esto incluye: strings, booleans, URLs, colores (custom_styles), números, selects
|
||||
- **Ejemplo real encontrado:** `admin-app.js:357` tiene `'Accede a más de 200,000...'` hardcodeado
|
||||
|
||||
---
|
||||
|
||||
### PREGUNTA 4: Propósito de los Valores en JS ✅ RESPONDIDA
|
||||
**¿Para qué se usan esos valores hardcodeados en JavaScript?**
|
||||
|
||||
Opciones:
|
||||
- [X] **Fallbacks cuando AJAX no retorna datos** ← USO PRINCIPAL
|
||||
- [X] Valores iniciales al renderizar formulario
|
||||
- [ ] Placeholders de campos de formulario
|
||||
- [ ] Valores de preview/demo
|
||||
- [ ] No estoy seguro
|
||||
- [X] **Botón "Reset to Defaults"** ← USO SECUNDARIO
|
||||
|
||||
**✅ Respuesta encontrada:**
|
||||
- **Uso 1 (líneas 117-129):** Fallbacks en método `render()` → `config.field || 'default'`
|
||||
- **Uso 2 (líneas 196-204):** Botón reset llama `loadConfig(DEFAULT_CONFIG)`
|
||||
- **Problema:** ❌ Los fallbacks son INNECESARIOS porque Settings Manager ya hace merge con defaults
|
||||
|
||||
---
|
||||
|
||||
### PREGUNTA 5: Comportamiento Esperado ✅ RESPONDIDA
|
||||
**¿Cómo DEBERÍAN manejarse los valores por defecto?**
|
||||
|
||||
Tu visión:
|
||||
- [X] Guardar en tabla `wp_apus_theme_components_defaults` (NUEVA tabla, NO la misma)
|
||||
- [X] Formato: Normalizado - un INSERT por campo (Opción A)
|
||||
- [X] JavaScript NUNCA debe tener defaults hardcodeados
|
||||
- [X] JavaScript debe leer defaults vía AJAX desde PHP
|
||||
- [X] PHP lee de tabla de defaults, NO tiene `get_defaults()` hardcodeado
|
||||
|
||||
**✅ Respuesta del usuario:** "opocion A, no debe ser en la msima tabla personalizada wp_apus_theme_components, debe ser en wp_apus_theme_components_defaults"
|
||||
|
||||
**Arquitectura definida:**
|
||||
1. Algoritmo extrae valores hardcodeados → Son los defaults
|
||||
2. Se insertan en tabla `wp_apus_theme_components_defaults` (un row por campo)
|
||||
3. Settings Manager lee de tabla de defaults
|
||||
4. JavaScript NO tiene `DEFAULT_CONFIG`
|
||||
5. JavaScript lee vía AJAX desde PHP
|
||||
|
||||
---
|
||||
|
||||
### PREGUNTA 6: Comparación con Sistema Actual ✅ RESPONDIDA
|
||||
**¿El componente Top Bar (que ya está implementado) tiene este problema?**
|
||||
|
||||
Verificación necesaria:
|
||||
```javascript
|
||||
// ¿Existe esto en admin-app.js?
|
||||
topBar.message_text || 'Accede a más de 200,000...'
|
||||
```
|
||||
|
||||
- [X] **SÍ - Top Bar tiene defaults hardcodeados en JS** ← CONFIRMADO
|
||||
- [ ] NO - Top Bar lee defaults correctamente de PHP
|
||||
- [ ] NO ESTOY SEGURO
|
||||
|
||||
**✅ Respuesta encontrada:**
|
||||
```javascript
|
||||
// admin-app.js:357
|
||||
document.getElementById('topBarMessageText').value = topBar.message_text || 'Accede a más de 200,000...';
|
||||
```
|
||||
|
||||
**Archivos con defaults de Top Bar:**
|
||||
1. `admin/includes/sanitizers/class-topbar-sanitizer.php` (línea 37)
|
||||
2. `admin/includes/class-settings-manager.php` (línea 84)
|
||||
3. `admin/assets/js/admin-app.js` (línea 357)
|
||||
4. `admin/pages/main.php` (líneas 243-244, 495)
|
||||
5. `admin/components/component-top-bar.php` (línea 190, 2 veces)
|
||||
6. `header.php` (línea 34)
|
||||
|
||||
**Total:** ❌ 7 lugares con el MISMO valor hardcodeado
|
||||
|
||||
---
|
||||
|
||||
### PREGUNTA 7: Alcance del Problema ✅ RESPONDIDA
|
||||
**¿Cuántos componentes están afectados?**
|
||||
|
||||
- [X] **Todos los componentes futuros que se modularicen** ← PREOCUPACIÓN PRINCIPAL
|
||||
|
||||
**✅ Respuesta:**
|
||||
- **Actual:** Código existente está MAL implementado - se debe eliminar y rehacer
|
||||
- **Futuro:** TODOS los componentes que se procesen con el algoritmo PASO 12/14 tendrán el mismo problema
|
||||
- **Crítico:** Si no se corrige el algoritmo PRIMERO, cada nuevo componente duplicará defaults en JS y PHP
|
||||
|
||||
**Acción requerida:**
|
||||
1. ❌ NO usar código actual como referencia (está mal hecho)
|
||||
2. ✅ Corregir algoritmo PRIMERO
|
||||
3. ✅ Limpiar panel de administración (eliminar rastros de componentes mal implementados)
|
||||
4. ✅ Limpiar tema (eliminar código duplicado)
|
||||
5. ✅ LUEGO ejecutar algoritmo corregido para cada componente
|
||||
|
||||
---
|
||||
|
||||
### PREGUNTA 8: Dónde Debe Estar la Única Fuente de Verdad ✅ RESPONDIDA
|
||||
**¿Dónde deben definirse los defaults UNA SOLA VEZ?**
|
||||
|
||||
Tu preferencia:
|
||||
- [X] **Tabla personalizada `wp_apus_theme_components_defaults`** ← ÚNICA FUENTE DE VERDAD
|
||||
- [X] Formato normalizado: un row por campo
|
||||
- [X] Se pobla mediante algoritmo al procesar cada componente
|
||||
- [ ] ❌ NO en `Settings Manager::get_defaults()` (eliminar método hardcodeado)
|
||||
- [ ] ❌ NO en JavaScript `DEFAULT_CONFIG` (eliminar objeto hardcodeado)
|
||||
|
||||
**✅ Respuesta del usuario:** Nueva tabla `wp_apus_theme_components_defaults` con estructura normalizada
|
||||
|
||||
**Flujo correcto:**
|
||||
```
|
||||
Algoritmo PASO 2-4
|
||||
↓
|
||||
Extrae valores hardcodeados del tema
|
||||
↓
|
||||
INSERT INTO wp_apus_theme_components_defaults
|
||||
↓
|
||||
Settings Manager lee de tabla
|
||||
↓
|
||||
JavaScript lee vía AJAX (sin fallbacks)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 OBJETIVO DE LA SOLUCIÓN
|
||||
|
||||
Una vez respondidas las preguntas, definiremos:
|
||||
|
||||
1. **Modificaciones al algoritmo** - Qué pasos cambiar
|
||||
2. **Nueva arquitectura de defaults** - Dónde y cómo guardarlos
|
||||
3. **Plan de migración** - Cómo corregir código existente
|
||||
4. **Validación** - Cómo verificar que la solución funciona
|
||||
|
||||
---
|
||||
|
||||
## 📝 CÓMO EL ALGORITMO EXTRAE DEFAULTS (REVISIÓN COMPLETA)
|
||||
|
||||
### Flujo Documentado en el Algoritmo
|
||||
|
||||
**PASO 2-4: Extraer valores hardcodeados del tema actual**
|
||||
- Usa Serena MCP para analizar archivos PHP/CSS/JS del tema
|
||||
- Identifica valores hardcodeados (textos, URLs, colores, iconos)
|
||||
- Documenta estos valores en `01-DOCUMENTACION-ANALISIS-CODIGO-REAL.md`
|
||||
|
||||
**PASO 6: Definir estructura JSON con defaults**
|
||||
- Toma los valores extraídos en PASO 2-4
|
||||
- Los define como valores por defecto en estructura JSON
|
||||
- Colores se extraen del CSS: `background-color: #0E2337` → `custom_styles.background_color: '#0E2337'`
|
||||
|
||||
**PASO 14 (Líneas 88-123): Implementar defaults en Settings Manager**
|
||||
```php
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
'version' => APUS_ADMIN_PANEL_VERSION,
|
||||
'components' => array(
|
||||
'component_name' => array(
|
||||
'enabled' => true,
|
||||
'field1' => 'Valor por defecto', // ← Del código hardcodeado actual
|
||||
'custom_styles' => array(
|
||||
'background_color' => '#0E2337', // ← Del CSS original
|
||||
'text_color' => '#ffffff' // ← Del CSS original
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ PROBLEMA: Defaults NO se insertan en tabla
|
||||
|
||||
**Lo que el algoritmo NO tiene:**
|
||||
- ❌ Script de inicialización que inserte defaults en `wp_apus_theme_components`
|
||||
- ❌ Paso que ejecute INSERT en la tabla al activar tema
|
||||
- ❌ Migrador que convierta defaults de PHP a DB
|
||||
|
||||
**Lo que el algoritmo SÍ tiene:**
|
||||
- ✅ Defaults en PHP (Settings Manager)
|
||||
- ✅ Settings Manager hace merge: `wp_parse_args($db_data, $defaults)`
|
||||
- ✅ Cuando tabla está vacía, usa defaults de PHP como fallback
|
||||
|
||||
### ✅ ENTENDIMIENTO CORRECTO DEL FLUJO:
|
||||
|
||||
**El algoritmo se ejecuta MANUALMENTE componente por componente:**
|
||||
|
||||
1. **Ejecutar algoritmo para "Top Bar":**
|
||||
- PASO 2-4: Extrae valores hardcodeados actuales de header.php, CSS, JS
|
||||
- Estos valores SON los defaults del Top Bar
|
||||
- PASO 14: ❌ Los pone en Settings Manager (PHP hardcodeado)
|
||||
- PASO 12: ❌ Los pone en JavaScript (DEFAULT_CONFIG hardcodeado)
|
||||
- ✅ DEBERÍA: Insertarlos en tabla `wp_apus_theme_components`
|
||||
|
||||
2. **Ejecutar algoritmo para "Navbar":**
|
||||
- PASO 2-4: Extrae valores hardcodeados actuales del navbar
|
||||
- Estos valores SON los defaults del Navbar
|
||||
- ✅ DEBERÍA: Insertarlos en tabla `wp_apus_theme_components`
|
||||
|
||||
3. **Y así con cada componente...**
|
||||
|
||||
### ❌ LO QUE ESTÁ MAL EN EL ALGORITMO:
|
||||
|
||||
**PASO 12:** Pone defaults en JavaScript
|
||||
**PASO 14:** Pone defaults en PHP Settings Manager
|
||||
|
||||
**✅ LO QUE DEBERÍA HACER:**
|
||||
|
||||
Agregar un NUEVO PASO (o modificar PASO 14) que:
|
||||
1. Tome los valores extraídos en PASO 2-4
|
||||
2. Los inserte en `wp_apus_theme_components` con INSERT INTO
|
||||
3. JavaScript NO tiene defaults hardcodeados
|
||||
4. PHP lee de tabla, NO tiene `get_defaults()` hardcodeado
|
||||
|
||||
## 📝 NOTAS ADICIONALES
|
||||
|
||||
_[Espacio para el usuario agregar información adicional]_
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 📊 RESUMEN EJECUTIVO DE HALLAZGOS
|
||||
|
||||
### ✅ Preguntas Respondidas (8 de 8) - INVESTIGACIÓN COMPLETA
|
||||
|
||||
| # | Pregunta | Respuesta |
|
||||
|---|----------|-----------|
|
||||
| 1 | ¿Dónde está el problema? | **PASO 12 y PASO 14** del algoritmo |
|
||||
| 2 | ¿Qué archivos JS afectados? | `component-*.js` (uno por componente) |
|
||||
| 3 | ¿Qué tipo de valores? | **TODOS** (strings, booleans, URLs, colores, etc.) |
|
||||
| 4 | ¿Para qué se usan? | Fallbacks + Botón Reset |
|
||||
| 5 | ¿Cómo DEBERÍAN manejarse? | Tabla `wp_apus_theme_components_defaults` normalizada |
|
||||
| 6 | ¿Top Bar tiene el problema? | **SÍ** - 7 lugares con mismo default |
|
||||
| 7 | ¿Cuántos componentes afectados? | Top Bar actual + TODOS los futuros |
|
||||
| 8 | ¿Única fuente de verdad? | Nueva tabla `wp_apus_theme_components_defaults` |
|
||||
|
||||
### 🎯 Decisión Arquitectónica Final
|
||||
|
||||
**ÚNICA FUENTE DE VERDAD:**
|
||||
- Tabla: `wp_apus_theme_components_defaults` (nueva)
|
||||
- Formato: Normalizado (un row por campo)
|
||||
- Poblamiento: Vía algoritmo al procesar cada componente
|
||||
- ❌ Eliminar: `DEFAULT_CONFIG` en JavaScript
|
||||
- ❌ Modificar: `get_defaults()` en PHP para leer de DB
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ ESTRUCTURA DE NUEVA TABLA DE DEFAULTS
|
||||
|
||||
### Tabla: `wp_apus_theme_components_defaults`
|
||||
|
||||
**Propósito:** Almacenar valores por defecto extraídos del tema mediante el algoritmo de modularización
|
||||
|
||||
**Características:**
|
||||
- ✅ Estructura normalizada (un row por campo)
|
||||
- ✅ Misma estructura que `wp_apus_theme_components` para consistencia
|
||||
- ✅ Se pobla automáticamente al ejecutar algoritmo para cada componente
|
||||
- ✅ Single source of truth para todos los defaults del sistema
|
||||
|
||||
### SQL Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS wp_apus_theme_components_defaults (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
component_name VARCHAR(50) NOT NULL COMMENT 'Nombre del componente (top_bar, navbar, hero, etc.)',
|
||||
config_key VARCHAR(100) NOT NULL COMMENT 'Clave de configuración (message_text, bg_color, etc.)',
|
||||
config_value TEXT NOT NULL COMMENT 'Valor por defecto extraído del tema',
|
||||
data_type ENUM('string','integer','boolean','array','json') NOT NULL COMMENT 'Tipo de dato del valor',
|
||||
version VARCHAR(20) DEFAULT NULL COMMENT 'Versión del tema cuando se insertó el default',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Fecha de creación del registro',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Última actualización',
|
||||
|
||||
UNIQUE KEY unique_default_config (component_name, config_key),
|
||||
INDEX idx_component_name (component_name),
|
||||
INDEX idx_config_key (config_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Valores por defecto de componentes del tema';
|
||||
```
|
||||
|
||||
### Estructura Genérica de Datos
|
||||
|
||||
```sql
|
||||
-- Estructura GENÉRICA para insertar defaults de CUALQUIER componente
|
||||
-- Se puebla al ejecutar algoritmo para cada componente
|
||||
|
||||
INSERT INTO wp_apus_theme_components_defaults
|
||||
(component_name, config_key, config_value, data_type, version)
|
||||
VALUES
|
||||
-- Campos booleanos
|
||||
('[component_name]', 'enabled', '[1|0]', 'boolean', '[version]'),
|
||||
('[component_name]', '[boolean_field]', '[1|0]', 'boolean', '[version]'),
|
||||
|
||||
-- Campos de texto (extraídos del código hardcodeado)
|
||||
('[component_name]', '[text_field]', '[valor_extraído_del_código]', 'string', '[version]'),
|
||||
|
||||
-- Campos numéricos
|
||||
('[component_name]', '[number_field]', '[valor_numérico]', 'integer', '[version]'),
|
||||
|
||||
-- Custom styles (extraídos del CSS del componente)
|
||||
('[component_name]', 'custom_styles.[propiedad_css]', '[valor_del_css]', 'string', '[version]');
|
||||
```
|
||||
|
||||
**Notas:**
|
||||
- `[component_name]`: Nombre del componente (ej: 'navbar', 'hero', 'footer')
|
||||
- `[config_key]`: Clave del campo según PASO 6 del algoritmo
|
||||
- `[config_value]`: Valor extraído del código/CSS actual del tema
|
||||
- `[data_type]`: Tipo según el campo (string, integer, boolean, array, json)
|
||||
- `[version]`: Versión del tema al momento de extraer defaults
|
||||
|
||||
### Propósito de Cada Tabla
|
||||
|
||||
**`wp_apus_theme_components`** (configuraciones personalizadas)
|
||||
- Se guardan cuando el usuario modifica valores en el Admin Panel
|
||||
- Si existe config personalizada, se usa esta
|
||||
|
||||
**`wp_apus_theme_components_defaults`** (valores por defecto)
|
||||
- Se pueblan al ejecutar el algoritmo para cada componente
|
||||
- Se usan SOLO cuando NO existe config personalizada
|
||||
- Son los valores extraídos del tema actual (hardcodeados)
|
||||
|
||||
**Ambas tablas tienen la MISMA estructura** - la diferencia es solo su propósito.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 FLUJO DE LECTURA DE DATOS
|
||||
|
||||
### ¿Por qué se necesitan Settings Manager (PHP) Y JavaScript?
|
||||
|
||||
**NO están duplicados - tienen propósitos diferentes:**
|
||||
|
||||
#### Settings Manager (PHP)
|
||||
**Propósito:** Para que archivos PHP del tema lean configuraciones
|
||||
|
||||
**Uso:**
|
||||
```php
|
||||
// En header.php, footer.php, etc.
|
||||
$settings_manager = new APUS_Settings_Manager();
|
||||
$navbar_config = $settings_manager->get_component_config('navbar');
|
||||
|
||||
// Usar $navbar_config en el HTML del tema
|
||||
echo $navbar_config['logo_url'];
|
||||
```
|
||||
|
||||
**Lee de:**
|
||||
1. Tabla `wp_apus_theme_components` (config personalizada) - PRIORIDAD ALTA
|
||||
2. Si no existe → Tabla `wp_apus_theme_components_defaults` (defaults)
|
||||
|
||||
#### JavaScript + AJAX
|
||||
**Propósito:** Para que el Admin Panel (interfaz de administración) lea/guarde configuraciones
|
||||
|
||||
**Uso:**
|
||||
```javascript
|
||||
// En admin-app.js
|
||||
// Leer configuración vía AJAX
|
||||
axios.get(ajaxUrl + '?action=get_component_config&component=navbar')
|
||||
.then(response => {
|
||||
// Renderizar formulario con los datos
|
||||
renderForm(response.data);
|
||||
});
|
||||
|
||||
// Guardar configuración vía AJAX
|
||||
axios.post(ajaxUrl, formData)
|
||||
.then(response => {
|
||||
// Mostrar mensaje de éxito
|
||||
});
|
||||
```
|
||||
|
||||
**Lee/Escribe vía AJAX a:**
|
||||
- Endpoint PHP que usa Settings Manager
|
||||
- Guarda en tabla `wp_apus_theme_components`
|
||||
|
||||
### Flujo Completo
|
||||
|
||||
```
|
||||
FRONTEND (tema):
|
||||
header.php → Settings Manager (PHP) → Lee de DB → Muestra en tema
|
||||
|
||||
ADMIN PANEL:
|
||||
JavaScript → AJAX → Endpoint PHP → Settings Manager → Lee/Escribe DB → Respuesta JSON
|
||||
```
|
||||
|
||||
**Conclusión:** Se necesitan AMBOS porque sirven a partes diferentes del sistema (frontend vs admin).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 PRÓXIMOS PASOS
|
||||
|
||||
### ✅ INVESTIGACIÓN COMPLETA - 8/8 preguntas respondidas
|
||||
|
||||
**ORDEN CORRECTO DE IMPLEMENTACIÓN:**
|
||||
|
||||
### FASE 1: LIMPIAR CÓDIGO ACTUAL (PRIMERO)
|
||||
|
||||
**El código actual está MAL implementado y debe eliminarse ANTES de corregir el algoritmo**
|
||||
|
||||
#### 1.1. Limpiar Panel de Administración
|
||||
**Eliminar completamente cualquier rastro de componentes mal implementados:**
|
||||
- Eliminar archivos JS de componentes: `admin/assets/js/component-*.js`
|
||||
- Eliminar archivos CSS de componentes: `admin/assets/css/component-*.css`
|
||||
- Eliminar archivos PHP de componentes: `admin/components/component-*.php`
|
||||
- Eliminar sanitizers de componentes: `admin/includes/sanitizers/class-*-sanitizer.php`
|
||||
- Limpiar `admin/pages/main.php` de secciones de componentes
|
||||
- Limpiar `admin/includes/class-admin-menu.php` de encolamiento de componentes
|
||||
|
||||
#### 1.2. Limpiar Tema
|
||||
**Eliminar valores hardcodeados duplicados:**
|
||||
- Revisar `header.php` y eliminar valores duplicados
|
||||
- Revisar otros archivos del tema con valores hardcodeados
|
||||
- Dejar SOLO el código original del tema (antes de modularización)
|
||||
|
||||
#### 1.3. Limpiar Base de Datos
|
||||
**Eliminar datos de componentes mal implementados:**
|
||||
- Vaciar tabla `wp_apus_theme_components` o eliminar componentes específicos
|
||||
- Preparar para empezar desde cero
|
||||
|
||||
---
|
||||
|
||||
### FASE 2: CREAR TABLA DE DEFAULTS
|
||||
|
||||
#### 2.1. Crear Tabla `wp_apus_theme_components_defaults`
|
||||
- Ejecutar SQL CREATE TABLE (ver estructura arriba)
|
||||
- Verificar que tabla existe y tiene estructura correcta
|
||||
|
||||
---
|
||||
|
||||
### FASE 3: CORREGIR ALGORITMO (DESPUÉS DE LIMPIAR)
|
||||
|
||||
#### 3.1. PASO 12: Implementar Admin JS (CORREGIR)
|
||||
**Archivo:** `00-algoritmo/12-F03-IMPLEMENTACION-IMPLEMENTAR-ADMIN-JS.md`
|
||||
|
||||
**Cambios:**
|
||||
- ❌ **ELIMINAR:** Objeto `DEFAULT_CONFIG` (líneas 43-51, 169-177)
|
||||
- ❌ **ELIMINAR:** Fallbacks en método `render()` (líneas 117-129)
|
||||
- ✅ **MODIFICAR:** Botón reset debe llamar endpoint AJAX para leer defaults de DB
|
||||
- ✅ JavaScript NUNCA tiene valores hardcodeados
|
||||
|
||||
#### 3.2. PASO 14: Settings Manager (CORREGIR)
|
||||
**Archivo:** `00-algoritmo/14-F04-CIERRE-GIT-COMMITS.md`
|
||||
|
||||
**Cambios:**
|
||||
- ❌ **ELIMINAR:** Método `get_defaults()` con array hardcodeado (líneas 88-123)
|
||||
- ✅ **MODIFICAR:** DB Manager para leer de tabla `_defaults`
|
||||
|
||||
#### 3.3. NUEVO PASO: Poblar Tabla de Defaults
|
||||
**Ubicación:** Después de PASO 7
|
||||
|
||||
**Contenido:**
|
||||
1. Leer valores extraídos en PASO 6 (estructura JSON)
|
||||
2. Generar script SQL con INSERTs para tabla `wp_apus_theme_components_defaults`
|
||||
3. Ejecutar script SQL
|
||||
4. Verificar que defaults están en DB
|
||||
|
||||
---
|
||||
|
||||
### FASE 4: USAR ALGORITMO CORREGIDO PARA DOCUMENTAR COMPONENTES
|
||||
|
||||
**El algoritmo NO implementa código - solo DOCUMENTA**
|
||||
|
||||
Una vez el algoritmo esté corregido:
|
||||
|
||||
#### 4.1. Ejecutar Algoritmo Completo (16 pasos) para UN Componente
|
||||
**Ejemplo:** Navbar
|
||||
|
||||
**Pasos 1-13: DOCUMENTACIÓN (genera 7 archivos MD)**
|
||||
- PASO 1: Crear issue en GitHub
|
||||
- PASO 2-4: Analizar código actual del Navbar (Serena MCP)
|
||||
- PASO 5-8: Diseñar campos configurables y estructura JSON
|
||||
- PASO 9-13: Documentar cómo implementar (plantillas, ejemplos, HTML, JS, CSS)
|
||||
|
||||
**OUTPUT:** Carpeta `navbar/` con 7 archivos MD de documentación
|
||||
|
||||
#### 4.2. PASO 14: Implementar Código Real
|
||||
**AQUÍ es cuando se modifica código PHP/JS/CSS del tema/admin**
|
||||
|
||||
⚠️ **NOTA IMPORTANTE:** El PASO 14 actual del algoritmo tiene el problema que identificamos:
|
||||
- Instruye crear método `get_defaults()` con array hardcodeado en Settings Manager (líneas 88-123)
|
||||
- Esto es lo que necesitamos CORREGIR en FASE 3
|
||||
|
||||
**Con el algoritmo CORREGIDO**, el PASO 14 debe hacer:
|
||||
|
||||
Usando la documentación generada en pasos 1-13:
|
||||
1. Modificar PHP del tema (ej: `header.php`) según `04-IMPLEMENTACION-COMPONENTE-NAVBAR.md`
|
||||
2. Agregar HTML admin en `admin/pages/main.php` según `05-IMPLEMENTACION-ADMIN-HTML-NAVBAR.md`
|
||||
3. Agregar JavaScript en `admin/assets/js/admin-app.js` según `07-IMPLEMENTACION-JS-ESPECIFICO.md`
|
||||
4. **MODIFICAR DB Manager:** Agregar método para insertar/leer defaults de tabla `wp_apus_theme_components_defaults`
|
||||
5. **MODIFICAR Settings Manager:** Leer de tabla defaults (NO array hardcodeado)
|
||||
6. **INSERTAR defaults en DB:** Ejecutar script SQL con valores extraídos en PASO 4
|
||||
7. Commits por cada archivo modificado
|
||||
|
||||
#### 4.3. PASO 15-16: Testing y Cierre
|
||||
- Testing post-implementación
|
||||
- Cerrar issue en GitHub
|
||||
|
||||
#### 4.4. Repetir para Cada Componente
|
||||
Una vez completado Navbar (pasos 1-16):
|
||||
1. Ejecutar algoritmo para siguiente componente (ej: Hero)
|
||||
2. Generar documentación (pasos 1-13)
|
||||
3. Implementar código real (paso 14)
|
||||
4. Testing y cierre (pasos 15-16)
|
||||
|
||||
**Componentes del tema a procesar:**
|
||||
- Navbar
|
||||
- Hero Section
|
||||
- Footer
|
||||
- etc.
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2025-01-13 - INVESTIGACIÓN COMPLETA - 8/8 preguntas respondidas
|
||||
**Estado:** 🟢 LISTO PARA IMPLEMENTACIÓN
|
||||
@@ -1,511 +0,0 @@
|
||||
/**
|
||||
* Admin Panel Styles
|
||||
*
|
||||
* Estilos base para el panel de administración
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
Container
|
||||
======================================== */
|
||||
|
||||
.apus-admin-panel {
|
||||
max-width: 1400px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Header
|
||||
======================================== */
|
||||
|
||||
.apus-admin-panel h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.apus-admin-panel .description {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Tabs
|
||||
======================================== */
|
||||
|
||||
.nav-tabs {
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
color: #666;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: #0073aa;
|
||||
border-bottom-color: #0073aa;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #0073aa;
|
||||
font-weight: 600;
|
||||
border-bottom-color: #0073aa;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Tab Content
|
||||
======================================== */
|
||||
|
||||
.tab-content {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab-pane h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tab-pane h4 {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Form Sections
|
||||
======================================== */
|
||||
|
||||
.form-section {
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="url"],
|
||||
.form-group input[type="email"],
|
||||
.form-group input[type="number"],
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-group .form-text {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-group .form-text code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Action Buttons
|
||||
======================================== */
|
||||
|
||||
.admin-actions {
|
||||
padding: 20px;
|
||||
background: #f9f9f9;
|
||||
border-top: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.admin-actions .button-primary {
|
||||
font-size: 14px;
|
||||
padding: 8px 20px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.admin-actions .button-primary i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Responsive
|
||||
======================================== */
|
||||
|
||||
@media (max-width: 782px) {
|
||||
.apus-admin-panel {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="url"],
|
||||
.form-group input[type="email"],
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
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,292 +0,0 @@
|
||||
/* ========================================
|
||||
ADMIN PANEL - NAVBAR COMPONENT STYLES
|
||||
Estilos compactos para el panel de administración
|
||||
======================================== */
|
||||
|
||||
/* Sobrescribir estilos del contenedor tab-content */
|
||||
.tab-content {
|
||||
padding: 24px 12px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Títulos de sección compactos */
|
||||
.section-title-compact {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-navy-primary);
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.section-title-compact i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Fix conflicto con WordPress: Sobreescribir max-width de .card */
|
||||
body .card,
|
||||
.apus-admin .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Fix conflicto con WordPress: Sobreescribir estilos de checkbox para que funcionen los switches de Bootstrap */
|
||||
.form-switch .form-check-input[type="checkbox"] {
|
||||
width: 2em !important;
|
||||
height: 1em !important;
|
||||
border-radius: 2em !important;
|
||||
background-color: var(--bs-form-check-bg) !important;
|
||||
background-image: var(--bs-form-switch-bg) !important;
|
||||
background-position: left center !important;
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
margin-left: -2.5em !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input[type="checkbox"]:checked {
|
||||
background-color: #0d6efd !important;
|
||||
border-color: #0d6efd !important;
|
||||
background-position: right center !important;
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !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='%23fff'/%3e%3c/svg%3e") !important;
|
||||
}
|
||||
|
||||
body .form-switch .form-check-input[type="checkbox"]::before,
|
||||
.apus-admin .form-switch .form-check-input[type="checkbox"]::before {
|
||||
content: none !important;
|
||||
display: none !important;
|
||||
background-image: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
body .form-switch .form-check-input[type="checkbox"]::after,
|
||||
.apus-admin .form-switch .form-check-input[type="checkbox"]::after {
|
||||
content: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Cards compactos */
|
||||
#navbarTab .card {
|
||||
border: 1px solid #dee2e6 !important;
|
||||
border-left: 4px solid #1e3a5f !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: rgba(0, 0, 0, 0.075) 0px 2px 4px 0px !important;
|
||||
transition: all 0.3s ease !important;
|
||||
background-color: #ffffff !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 0 16px !important;
|
||||
}
|
||||
|
||||
.form-section.card {
|
||||
border: 2px solid #d1d5db !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-section.card:hover {
|
||||
border-color: #FF8600 !important;
|
||||
box-shadow: 0 4px 16px rgba(255, 134, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.form-section.card .card-body.p-3 {
|
||||
padding: 1.25rem !important;
|
||||
}
|
||||
|
||||
/* Color pickers compactos */
|
||||
.form-control-color-compact {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--color-neutral-100);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control-color-compact:hover {
|
||||
border-color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
/* Form controls pequeños con mejor UX */
|
||||
#navbarTab .form-control-sm,
|
||||
#navbarTab .form-select-sm {
|
||||
font-size: 14px !important;
|
||||
padding: 6px 12px !important;
|
||||
border: 1px solid rgb(222, 226, 230) !important;
|
||||
border-radius: 4px !important;
|
||||
color: rgb(33, 37, 41) !important;
|
||||
}
|
||||
|
||||
.form-control-sm,
|
||||
.form-select-sm {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Labels compactos */
|
||||
.form-label.small {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-neutral-700);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Fix alineación vertical de labels en switches */
|
||||
#navbarTab .form-check.form-switch {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
#navbarTab .form-switch .form-check-input {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
#navbarTab .form-switch .form-check-label,
|
||||
.form-switch .form-check-label.small {
|
||||
line-height: 16px !important;
|
||||
padding-top: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Switches más compactos */
|
||||
.form-check-switch .form-check-input {
|
||||
width: 2.5rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Reducir gaps globales */
|
||||
.row.g-3 {
|
||||
--bs-gutter-x: 1rem;
|
||||
--bs-gutter-y: 1rem;
|
||||
}
|
||||
|
||||
.row.g-2 {
|
||||
--bs-gutter-x: 0.5rem;
|
||||
--bs-gutter-y: 0.5rem;
|
||||
}
|
||||
|
||||
/* Badges pequeños */
|
||||
.badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2em 0.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Tab header compacto */
|
||||
.tab-header {
|
||||
background: linear-gradient(135deg, #0E2337 0%, #1a3a5c 100%);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 2px solid #FF8600;
|
||||
box-shadow: 0 4px 12px rgba(14, 35, 55, 0.15);
|
||||
}
|
||||
|
||||
.tab-header h3 {
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab-header p {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
/* Header específico del Navbar Tab */
|
||||
#navbarTab > .rounded {
|
||||
background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%) !important;
|
||||
border-left: 4px solid #FF8600 !important;
|
||||
border-radius: 6px !important;
|
||||
padding: 24px !important;
|
||||
margin-bottom: 24px !important;
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 8px 16px 0px !important;
|
||||
}
|
||||
|
||||
#navbarTab > .rounded h3 {
|
||||
font-size: 1.5rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
#navbarTab > .rounded p {
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Card body padding */
|
||||
#navbarTab .card-body {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
/* Progress bar pequeña */
|
||||
.progress[style*="height: 3px"] {
|
||||
border-radius: 2px;
|
||||
background-color: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
/* Botones pequeños */
|
||||
.btn-sm {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Botón primario con estilo de marca */
|
||||
.btn-primary {
|
||||
background-color: var(--color-orange-primary);
|
||||
border-color: var(--color-orange-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--color-orange-hover);
|
||||
border-color: var(--color-orange-hover);
|
||||
}
|
||||
|
||||
/* Clases de utilidad de colores */
|
||||
.text-navy-primary {
|
||||
color: var(--color-navy-primary);
|
||||
}
|
||||
|
||||
.text-orange-primary {
|
||||
color: var(--color-orange-primary);
|
||||
}
|
||||
|
||||
.text-neutral-600 {
|
||||
color: var(--color-neutral-600);
|
||||
}
|
||||
|
||||
.border-neutral-200 {
|
||||
border-color: #dee2e6 !important;
|
||||
}
|
||||
|
||||
/* Responsive: Mobile First */
|
||||
@media (max-width: 991px) {
|
||||
.tab-header {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.tab-header h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.form-section.card .card-body.p-3 {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.tab-header .d-flex {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.tab-header button {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
/**
|
||||
* Theme Options Admin Styles
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/* Main Container */
|
||||
.apus-theme-options {
|
||||
margin: 20px 20px 0 0;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.apus-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);
|
||||
}
|
||||
|
||||
.apus-options-logo h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #1d2327;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.apus-options-logo .version {
|
||||
background: #2271b1;
|
||||
color: #fff;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.apus-options-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.apus-options-actions .button .dashicons {
|
||||
margin-top: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.apus-options-form {
|
||||
background: #fff;
|
||||
border: 1px solid #c3c4c7;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||
}
|
||||
|
||||
/* Tabs Container */
|
||||
.apus-options-container {
|
||||
display: flex;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* Tabs Navigation */
|
||||
.apus-tabs-nav {
|
||||
width: 200px;
|
||||
background: #f6f7f7;
|
||||
border-right: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.apus-tabs-nav ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.apus-tabs-nav li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.apus-tabs-nav li:first-child {
|
||||
border-top: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.apus-tabs-nav a {
|
||||
display: block;
|
||||
padding: 15px 20px;
|
||||
color: #50575e;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.apus-tabs-nav a .dashicons {
|
||||
margin-right: 8px;
|
||||
color: #787c82;
|
||||
}
|
||||
|
||||
.apus-tabs-nav a:hover {
|
||||
background: #fff;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.apus-tabs-nav a:hover .dashicons {
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.apus-tabs-nav li.active a {
|
||||
background: #fff;
|
||||
color: #2271b1;
|
||||
font-weight: 600;
|
||||
border-left: 3px solid #2271b1;
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
.apus-tabs-nav li.active a .dashicons {
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
/* Tabs Content */
|
||||
.apus-tabs-content {
|
||||
flex: 1;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.apus-tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.apus-tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.apus-tab-pane h2 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 23px;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.apus-tab-pane > p.description {
|
||||
margin: 0 0 20px 0;
|
||||
color: #646970;
|
||||
}
|
||||
|
||||
.apus-tab-pane h3 {
|
||||
margin: 30px 0 0 0;
|
||||
padding: 15px 0 10px 0;
|
||||
border-top: 1px solid #dcdcde;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Form Table */
|
||||
.apus-tab-pane .form-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.apus-tab-pane .form-table th {
|
||||
padding: 20px 10px 20px 0;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.apus-tab-pane .form-table td {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.apus-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.apus-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.apus-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.apus-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .apus-slider {
|
||||
background-color: #2271b1;
|
||||
}
|
||||
|
||||
input:focus + .apus-slider {
|
||||
box-shadow: 0 0 1px #2271b1;
|
||||
}
|
||||
|
||||
input:checked + .apus-slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Image Upload */
|
||||
.apus-image-upload {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.apus-image-preview {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #c3c4c7;
|
||||
background: #f6f7f7;
|
||||
padding: 10px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.apus-image-preview:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.apus-preview-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.apus-upload-image,
|
||||
.apus-remove-image {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Submit Button */
|
||||
.apus-options-form .submit {
|
||||
margin: 0;
|
||||
padding: 20px 30px;
|
||||
border-top: 1px solid #c3c4c7;
|
||||
background: #f6f7f7;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.apus-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);
|
||||
}
|
||||
|
||||
.apus-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;
|
||||
}
|
||||
|
||||
.apus-modal-close {
|
||||
color: #646970;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.apus-modal-close:hover,
|
||||
.apus-modal-close:focus {
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.apus-modal-content h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.apus-modal-content textarea {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Notices */
|
||||
.apus-notice {
|
||||
padding: 12px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||
}
|
||||
|
||||
.apus-notice.success {
|
||||
border-left-color: #00a32a;
|
||||
}
|
||||
|
||||
.apus-notice.error {
|
||||
border-left-color: #d63638;
|
||||
}
|
||||
|
||||
.apus-notice.warning {
|
||||
border-left-color: #dba617;
|
||||
}
|
||||
|
||||
.apus-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) {
|
||||
.apus-options-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.apus-tabs-nav {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.apus-tabs-nav ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.apus-tabs-nav li {
|
||||
flex: 1;
|
||||
min-width: 50%;
|
||||
border-right: 1px solid #c3c4c7;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.apus-tabs-nav li:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.apus-tabs-nav a {
|
||||
text-align: center;
|
||||
padding: 12px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.apus-tabs-nav a .dashicons {
|
||||
display: block;
|
||||
margin: 0 auto 5px;
|
||||
}
|
||||
|
||||
.apus-tabs-nav li.active a {
|
||||
border-left: none;
|
||||
border-bottom: 3px solid #2271b1;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.apus-tabs-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.apus-options-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.apus-options-actions {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.apus-options-actions .button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.apus-tab-pane .form-table th {
|
||||
width: auto;
|
||||
padding: 15px 10px 5px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.apus-tab-pane .form-table td {
|
||||
display: block;
|
||||
padding: 5px 10px 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.apus-spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(0,0,0,.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: #2271b1;
|
||||
animation: apus-spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes apus-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Helper Classes */
|
||||
.apus-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.apus-text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.apus-mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.apus-mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Color Picker */
|
||||
.wp-picker-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Field Dependencies */
|
||||
.apus-field-dependency {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Success Animation */
|
||||
@keyframes apus-saved {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.apus-saved {
|
||||
animation: apus-saved 0.3s ease-in-out;
|
||||
}
|
||||
@@ -1,430 +0,0 @@
|
||||
/**
|
||||
* Admin Panel Application
|
||||
*
|
||||
* Gestión de configuraciones de componentes del tema
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
const AdminPanel = {
|
||||
/**
|
||||
* Estado de la aplicación
|
||||
*/
|
||||
STATE: {
|
||||
settings: {},
|
||||
hasChanges: false,
|
||||
isLoading: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Inicializar aplicación
|
||||
*/
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.loadSettings();
|
||||
},
|
||||
|
||||
/**
|
||||
* Vincular eventos
|
||||
*/
|
||||
bindEvents() {
|
||||
// Botón guardar
|
||||
document.getElementById('saveSettings').addEventListener('click', () => {
|
||||
this.saveSettings();
|
||||
});
|
||||
|
||||
// Detectar cambios en formularios
|
||||
const enableSaveButton = () => {
|
||||
this.STATE.hasChanges = true;
|
||||
document.getElementById('saveSettings').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);
|
||||
});
|
||||
});
|
||||
|
||||
// Contador de caracteres
|
||||
this.setupCharacterCounter();
|
||||
|
||||
// Vista previa en tiempo real
|
||||
this.setupLivePreview();
|
||||
},
|
||||
|
||||
/**
|
||||
* Configurar contador de caracteres para textarea
|
||||
*/
|
||||
setupCharacterCounter() {
|
||||
const messageTextarea = document.getElementById('topBarMessageText');
|
||||
if (!messageTextarea) return;
|
||||
|
||||
messageTextarea.addEventListener('input', () => {
|
||||
const count = messageTextarea.value.length;
|
||||
const maxLength = 250;
|
||||
const percentage = (count / maxLength) * 100;
|
||||
|
||||
const counter = document.getElementById('topBarMessageTextCount');
|
||||
const progress = document.getElementById('topBarMessageTextProgress');
|
||||
|
||||
if (counter) {
|
||||
counter.textContent = count;
|
||||
|
||||
// Cambiar color según proximidad al límite
|
||||
counter.classList.remove('text-danger', 'text-warning', 'text-muted');
|
||||
|
||||
if (count > 230) {
|
||||
counter.classList.add('text-danger');
|
||||
} else if (count > 200) {
|
||||
counter.classList.add('text-warning');
|
||||
} else {
|
||||
counter.classList.add('text-muted');
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar progress bar
|
||||
if (progress) {
|
||||
progress.style.width = percentage + '%';
|
||||
progress.setAttribute('aria-valuenow', count);
|
||||
|
||||
// Cambiar color del progress bar
|
||||
progress.classList.remove('bg-orange-primary', 'bg-warning', 'bg-danger');
|
||||
if (count > 230) {
|
||||
progress.classList.add('bg-danger');
|
||||
} else if (count > 200) {
|
||||
progress.classList.add('bg-warning');
|
||||
} else {
|
||||
progress.classList.add('bg-orange-primary');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger inicial para mostrar count actual
|
||||
messageTextarea.dispatchEvent(new Event('input'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Configurar vista previa en tiempo real
|
||||
*/
|
||||
setupLivePreview() {
|
||||
const preview = document.getElementById('topBarPreview');
|
||||
if (!preview) return;
|
||||
|
||||
// Campos que afectan la vista previa
|
||||
const fields = {
|
||||
iconClass: document.getElementById('topBarIconClass'),
|
||||
showIcon: document.getElementById('topBarShowIcon'),
|
||||
highlightText: document.getElementById('topBarHighlightText'),
|
||||
messageText: document.getElementById('topBarMessageText'),
|
||||
linkText: document.getElementById('topBarLinkText'),
|
||||
showLink: document.getElementById('topBarShowLink'),
|
||||
bgColor: document.getElementById('topBarBgColor'),
|
||||
textColor: document.getElementById('topBarTextColor'),
|
||||
highlightColor: document.getElementById('topBarHighlightColor'),
|
||||
fontSize: document.getElementById('topBarFontSize')
|
||||
};
|
||||
|
||||
// Función para actualizar la vista previa
|
||||
const updatePreview = () => {
|
||||
// Obtener valores
|
||||
const iconClass = fields.iconClass.value || 'bi bi-megaphone-fill';
|
||||
const showIcon = fields.showIcon.checked;
|
||||
const highlightText = fields.highlightText.value;
|
||||
const messageText = fields.messageText.value || 'Tu mensaje aquí...';
|
||||
const linkText = fields.linkText.value || 'Ver más';
|
||||
const showLink = fields.showLink.checked;
|
||||
const bgColor = fields.bgColor.value || '#0E2337';
|
||||
const textColor = fields.textColor.value || '#ffffff';
|
||||
const highlightColor = fields.highlightColor.value || '#FF8600';
|
||||
|
||||
// Mapeo de tamaños de fuente
|
||||
const fontSizeMap = {
|
||||
'small': '0.8rem',
|
||||
'normal': '0.9rem',
|
||||
'large': '1rem'
|
||||
};
|
||||
const fontSize = fontSizeMap[fields.fontSize.value] || '0.9rem';
|
||||
|
||||
// Construir HTML de la vista previa
|
||||
let html = '';
|
||||
|
||||
// Icono
|
||||
if (showIcon && iconClass) {
|
||||
html += `<i class="${iconClass}" style="font-size: 1.2rem; color: ${highlightColor};"></i>`;
|
||||
}
|
||||
|
||||
// Texto destacado
|
||||
if (highlightText) {
|
||||
html += `<span style="font-weight: 700; color: ${highlightColor};">${highlightText}</span>`;
|
||||
}
|
||||
|
||||
// Mensaje principal
|
||||
html += `<span style="flex: 1; min-width: 300px; text-align: center;">${messageText}</span>`;
|
||||
|
||||
// Enlace
|
||||
if (showLink && linkText) {
|
||||
html += `<a href="#" style="color: ${textColor}; text-decoration: underline; white-space: nowrap; transition: color 0.3s;">${linkText}</a>`;
|
||||
}
|
||||
|
||||
// Actualizar la vista previa
|
||||
preview.innerHTML = html;
|
||||
preview.style.backgroundColor = bgColor;
|
||||
preview.style.color = textColor;
|
||||
preview.style.fontSize = fontSize;
|
||||
};
|
||||
|
||||
// Agregar listeners a todos los campos
|
||||
Object.values(fields).forEach(field => {
|
||||
if (field) {
|
||||
field.addEventListener('input', updatePreview);
|
||||
field.addEventListener('change', updatePreview);
|
||||
}
|
||||
});
|
||||
|
||||
// Actualización inicial
|
||||
updatePreview();
|
||||
},
|
||||
|
||||
/**
|
||||
* Cambiar tab
|
||||
*/
|
||||
switchTab(tab) {
|
||||
// Remover active de todos
|
||||
document.querySelectorAll('.nav-tabs .nav-link').forEach(t => {
|
||||
t.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => {
|
||||
pane.classList.remove('show', 'active');
|
||||
});
|
||||
|
||||
// Activar seleccionado
|
||||
tab.classList.add('active');
|
||||
const targetId = tab.getAttribute('data-bs-target').substring(1);
|
||||
const targetPane = document.getElementById(targetId);
|
||||
if (targetPane) {
|
||||
targetPane.classList.add('show', 'active');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cargar configuraciones desde servidor
|
||||
*/
|
||||
async loadSettings() {
|
||||
this.STATE.isLoading = true;
|
||||
this.showSpinner(true);
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
url: apusAdminData.ajaxUrl,
|
||||
data: new URLSearchParams({
|
||||
action: 'apus_get_settings',
|
||||
nonce: apusAdminData.nonce
|
||||
})
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
this.STATE.settings = response.data.data;
|
||||
this.renderAllComponents();
|
||||
} else {
|
||||
this.showNotice('Error al cargar configuraciones', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
this.showNotice('Error de conexión', 'error');
|
||||
} finally {
|
||||
this.STATE.isLoading = false;
|
||||
this.showSpinner(false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Guardar configuraciones al servidor
|
||||
*/
|
||||
async saveSettings() {
|
||||
if (!this.STATE.hasChanges) {
|
||||
this.showNotice('No hay cambios para guardar', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
this.showSpinner(true);
|
||||
|
||||
try {
|
||||
const formData = this.collectFormData();
|
||||
|
||||
// Crear FormData para WordPress AJAX
|
||||
const postData = new URLSearchParams();
|
||||
postData.append('action', 'apus_save_settings');
|
||||
postData.append('nonce', apusAdminData.nonce);
|
||||
|
||||
// Agregar components como JSON string
|
||||
postData.append('components', JSON.stringify(formData.components));
|
||||
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
url: apusAdminData.ajaxUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
data: postData
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
this.STATE.hasChanges = false;
|
||||
document.getElementById('saveSettings').disabled = true;
|
||||
this.showNotice('Configuración guardada correctamente', 'success');
|
||||
} else {
|
||||
this.showNotice(response.data.data.message || 'Error al guardar', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
this.showNotice('Error de conexión', 'error');
|
||||
} finally {
|
||||
this.showSpinner(false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Recolectar datos del formulario
|
||||
* Cada componente agregará su sección aquí
|
||||
*/
|
||||
collectFormData() {
|
||||
return {
|
||||
components: {
|
||||
top_bar: {
|
||||
enabled: document.getElementById('topBarEnabled').checked,
|
||||
show_on_mobile: document.getElementById('topBarShowOnMobile').checked,
|
||||
show_on_desktop: document.getElementById('topBarShowOnDesktop').checked,
|
||||
icon_class: document.getElementById('topBarIconClass').value.trim(),
|
||||
show_icon: document.getElementById('topBarShowIcon').checked,
|
||||
highlight_text: document.getElementById('topBarHighlightText').value.trim(),
|
||||
message_text: document.getElementById('topBarMessageText').value.trim(),
|
||||
link_text: document.getElementById('topBarLinkText').value.trim(),
|
||||
link_url: document.getElementById('topBarLinkUrl').value.trim(),
|
||||
link_target: document.getElementById('topBarLinkTarget').value,
|
||||
show_link: document.getElementById('topBarShowLink').checked,
|
||||
custom_styles: {
|
||||
background_color: this.getColorValue('topBarBgColor', ''),
|
||||
text_color: this.getColorValue('topBarTextColor', ''),
|
||||
highlight_color: this.getColorValue('topBarHighlightColor', ''),
|
||||
link_hover_color: this.getColorValue('topBarLinkHoverColor', ''),
|
||||
font_size: document.getElementById('topBarFontSize').value
|
||||
}
|
||||
}
|
||||
// Navbar - Pendiente
|
||||
// Hero - Pendiente
|
||||
// Footer - Pendiente
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Renderizar todos los componentes
|
||||
*/
|
||||
renderAllComponents() {
|
||||
const components = this.STATE.settings.components || {};
|
||||
|
||||
// Top Bar
|
||||
if (components.top_bar) {
|
||||
this.renderTopBar(components.top_bar);
|
||||
}
|
||||
// Navbar - Pendiente
|
||||
// Hero - Pendiente
|
||||
// Footer - Pendiente
|
||||
},
|
||||
|
||||
/**
|
||||
* Renderizar Top Bar
|
||||
*/
|
||||
renderTopBar(topBar) {
|
||||
document.getElementById('topBarEnabled').checked = topBar.enabled !== undefined ? topBar.enabled : true;
|
||||
document.getElementById('topBarShowOnMobile').checked = topBar.show_on_mobile !== undefined ? topBar.show_on_mobile : true;
|
||||
document.getElementById('topBarShowOnDesktop').checked = topBar.show_on_desktop !== undefined ? topBar.show_on_desktop : true;
|
||||
document.getElementById('topBarIconClass').value = topBar.icon_class || 'bi bi-megaphone-fill';
|
||||
document.getElementById('topBarShowIcon').checked = topBar.show_icon !== undefined ? topBar.show_icon : true;
|
||||
document.getElementById('topBarHighlightText').value = topBar.highlight_text || 'Nuevo:';
|
||||
document.getElementById('topBarMessageText').value = topBar.message_text || 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.';
|
||||
document.getElementById('topBarLinkText').value = topBar.link_text || 'Ver Catálogo';
|
||||
document.getElementById('topBarLinkUrl').value = topBar.link_url || '/catalogo';
|
||||
document.getElementById('topBarLinkTarget').value = topBar.link_target || '_self';
|
||||
document.getElementById('topBarShowLink').checked = topBar.show_link !== undefined ? topBar.show_link : true;
|
||||
|
||||
// Estilos personalizados
|
||||
if (topBar.custom_styles) {
|
||||
if (topBar.custom_styles.background_color) {
|
||||
document.getElementById('topBarBgColor').value = topBar.custom_styles.background_color;
|
||||
}
|
||||
if (topBar.custom_styles.text_color) {
|
||||
document.getElementById('topBarTextColor').value = topBar.custom_styles.text_color;
|
||||
}
|
||||
if (topBar.custom_styles.highlight_color) {
|
||||
document.getElementById('topBarHighlightColor').value = topBar.custom_styles.highlight_color;
|
||||
}
|
||||
if (topBar.custom_styles.link_hover_color) {
|
||||
document.getElementById('topBarLinkHoverColor').value = topBar.custom_styles.link_hover_color;
|
||||
}
|
||||
document.getElementById('topBarFontSize').value = topBar.custom_styles.font_size || 'normal';
|
||||
}
|
||||
|
||||
// Trigger contador de caracteres
|
||||
const messageTextarea = document.getElementById('topBarMessageText');
|
||||
if (messageTextarea) {
|
||||
messageTextarea.dispatchEvent(new Event('input'));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Utilidad: Obtener valor de color con fallback
|
||||
*/
|
||||
getColorValue(inputId, defaultValue) {
|
||||
const input = document.getElementById(inputId);
|
||||
const value = input ? input.value.trim() : '';
|
||||
return value || defaultValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Utilidad: Mostrar spinner
|
||||
*/
|
||||
showSpinner(show) {
|
||||
const spinner = document.querySelector('.spinner');
|
||||
if (spinner) {
|
||||
spinner.style.display = show ? 'inline-block' : 'none';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Utilidad: Mostrar notificación
|
||||
*/
|
||||
showNotice(message, type = 'info') {
|
||||
// WordPress admin notices
|
||||
const noticeDiv = document.createElement('div');
|
||||
noticeDiv.className = `notice notice-${type} is-dismissible`;
|
||||
noticeDiv.innerHTML = `<p>${message}</p>`;
|
||||
|
||||
const container = document.querySelector('.apus-admin-panel');
|
||||
if (container) {
|
||||
container.insertBefore(noticeDiv, container.firstChild);
|
||||
|
||||
// Auto-dismiss después de 5 segundos
|
||||
setTimeout(() => {
|
||||
noticeDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Inicializar cuando el DOM esté listo
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
AdminPanel.init();
|
||||
});
|
||||
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
* Navbar Component - JavaScript Controller
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
window.NavbarComponent = {
|
||||
/**
|
||||
* Inicialización del componente
|
||||
*/
|
||||
init: function() {
|
||||
console.log('Navbar component initialized');
|
||||
|
||||
// Actualizar valores hexadecimales de color pickers en tiempo real
|
||||
this.initColorPickers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inicializar event listeners para color pickers
|
||||
*/
|
||||
initColorPickers: function() {
|
||||
const colorPickers = [
|
||||
{ input: 'navbarBgColor', display: 'navbarBgColorValue' },
|
||||
{ input: 'navbarTextColor', display: 'navbarTextColorValue' },
|
||||
{ input: 'navbarLinkHoverColor', display: 'navbarLinkHoverColorValue' },
|
||||
{ input: 'navbarLinkHoverBgColor', display: 'navbarLinkHoverBgColorValue' },
|
||||
{ input: 'navbarDropdownBgColor', display: 'navbarDropdownBgColorValue' },
|
||||
{ input: 'navbarDropdownItemColor', display: 'navbarDropdownItemColorValue' },
|
||||
{ input: 'navbarDropdownItemHoverColor', display: 'navbarDropdownItemHoverColorValue' }
|
||||
];
|
||||
|
||||
colorPickers.forEach(function(picker) {
|
||||
const inputEl = document.getElementById(picker.input);
|
||||
const displayEl = document.getElementById(picker.display);
|
||||
|
||||
if (inputEl && displayEl) {
|
||||
// Actualizar cuando cambia el color
|
||||
inputEl.addEventListener('input', function() {
|
||||
displayEl.textContent = inputEl.value.toUpperCase();
|
||||
});
|
||||
|
||||
// Inicializar valor al cargar
|
||||
displayEl.textContent = inputEl.value.toUpperCase();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Recolectar datos del formulario
|
||||
* @returns {Object} Configuración del navbar
|
||||
*/
|
||||
collect: function() {
|
||||
return {
|
||||
// Grupo 1: Activación y Visibilidad
|
||||
enabled: document.getElementById('navbarEnabled').checked,
|
||||
show_on_mobile: document.getElementById('navbarShowOnMobile').checked,
|
||||
show_on_desktop: document.getElementById('navbarShowOnDesktop').checked,
|
||||
position: document.getElementById('navbarPosition').value,
|
||||
responsive_breakpoint: document.getElementById('navbarBreakpoint').value,
|
||||
|
||||
// Grupo 4: Efectos (booleans)
|
||||
enable_box_shadow: document.getElementById('navbarEnableBoxShadow').checked,
|
||||
enable_underline_effect: document.getElementById('navbarEnableUnderlineEffect').checked,
|
||||
enable_hover_background: document.getElementById('navbarEnableHoverBackground').checked,
|
||||
|
||||
// Grupo 6: Let's Talk Button
|
||||
lets_talk_button: {
|
||||
enabled: document.getElementById('navbarLetsTalkEnabled').checked,
|
||||
text: document.getElementById('navbarLetsTalkText').value.trim(),
|
||||
icon_class: document.getElementById('navbarLetsTalkIconClass').value.trim(),
|
||||
show_icon: document.getElementById('navbarLetsTalkShowIcon').checked,
|
||||
position: document.getElementById('navbarLetsTalkPosition').value
|
||||
},
|
||||
|
||||
// Grupo 7: Dropdown
|
||||
dropdown: {
|
||||
enable_hover_desktop: document.getElementById('navbarDropdownEnableHoverDesktop').checked,
|
||||
max_height: parseInt(document.getElementById('navbarDropdownMaxHeight').value) || 70,
|
||||
border_radius: parseInt(document.getElementById('navbarDropdownBorderRadius').value) || 8,
|
||||
item_padding_vertical: parseFloat(document.getElementById('navbarDropdownItemPaddingVertical').value) || 0.5,
|
||||
item_padding_horizontal: parseFloat(document.getElementById('navbarDropdownItemPaddingHorizontal').value) || 1.25
|
||||
},
|
||||
|
||||
// Grupos 2, 3, 5: Custom Styles
|
||||
custom_styles: {
|
||||
// Grupo 2: Colores
|
||||
background_color: document.getElementById('navbarBgColor').value || '#1e3a5f',
|
||||
text_color: document.getElementById('navbarTextColor').value || '#ffffff',
|
||||
link_hover_color: document.getElementById('navbarLinkHoverColor').value || '#FF8600',
|
||||
link_hover_bg_color: document.getElementById('navbarLinkHoverBgColor').value || '#FF8600',
|
||||
dropdown_bg_color: document.getElementById('navbarDropdownBgColor').value || '#ffffff',
|
||||
dropdown_item_color: document.getElementById('navbarDropdownItemColor').value || '#4A5568',
|
||||
dropdown_item_hover_color: document.getElementById('navbarDropdownItemHoverColor').value || '#FF8600',
|
||||
|
||||
// Grupo 3: Tipografía
|
||||
font_size: document.getElementById('navbarFontSize').value,
|
||||
font_weight: document.getElementById('navbarFontWeight').value,
|
||||
|
||||
// Grupo 4: Efectos
|
||||
box_shadow_intensity: document.getElementById('navbarBoxShadowIntensity').value,
|
||||
border_radius: parseInt(document.getElementById('navbarBorderRadius').value) || 4,
|
||||
|
||||
// Grupo 5: Spacing
|
||||
padding_vertical: parseFloat(document.getElementById('navbarPaddingVertical').value) || 0.75,
|
||||
link_padding_vertical: parseFloat(document.getElementById('navbarLinkPaddingVertical').value) || 0.5,
|
||||
link_padding_horizontal: parseFloat(document.getElementById('navbarLinkPaddingHorizontal').value) || 0.65,
|
||||
|
||||
// Grupo 8: Avanzado
|
||||
z_index: parseInt(document.getElementById('navbarZIndex').value) || 1030,
|
||||
transition_speed: document.getElementById('navbarTransitionSpeed').value || 'normal'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Renderizar configuración en el formulario
|
||||
* @param {Object} config - Configuración del navbar
|
||||
*/
|
||||
render: function(config) {
|
||||
if (!config) {
|
||||
console.warn('No navbar config provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// Grupo 1: Activación y Visibilidad
|
||||
document.getElementById('navbarEnabled').checked = config.enabled !== undefined ? config.enabled : true;
|
||||
document.getElementById('navbarShowOnMobile').checked = config.show_on_mobile !== undefined ? config.show_on_mobile : true;
|
||||
document.getElementById('navbarShowOnDesktop').checked = config.show_on_desktop !== undefined ? config.show_on_desktop : true;
|
||||
document.getElementById('navbarPosition').value = config.position || 'sticky';
|
||||
document.getElementById('navbarBreakpoint').value = config.responsive_breakpoint || 'lg';
|
||||
|
||||
// Grupo 2: Colores Personalizados
|
||||
if (config.custom_styles) {
|
||||
document.getElementById('navbarBgColor').value = config.custom_styles.background_color || '#1e3a5f';
|
||||
document.getElementById('navbarTextColor').value = config.custom_styles.text_color || '#ffffff';
|
||||
document.getElementById('navbarLinkHoverColor').value = config.custom_styles.link_hover_color || '#FF8600';
|
||||
document.getElementById('navbarLinkHoverBgColor').value = config.custom_styles.link_hover_bg_color || '#FF8600';
|
||||
document.getElementById('navbarDropdownBgColor').value = config.custom_styles.dropdown_bg_color || '#ffffff';
|
||||
document.getElementById('navbarDropdownItemColor').value = config.custom_styles.dropdown_item_color || '#4A5568';
|
||||
document.getElementById('navbarDropdownItemHoverColor').value = config.custom_styles.dropdown_item_hover_color || '#FF8600';
|
||||
}
|
||||
|
||||
// Grupo 3: Tipografía
|
||||
if (config.custom_styles) {
|
||||
document.getElementById('navbarFontSize').value = config.custom_styles.font_size || 'normal';
|
||||
document.getElementById('navbarFontWeight').value = config.custom_styles.font_weight || '500';
|
||||
}
|
||||
|
||||
// Grupo 4: Efectos Visuales
|
||||
document.getElementById('navbarEnableBoxShadow').checked = config.enable_box_shadow !== undefined ? config.enable_box_shadow : true;
|
||||
document.getElementById('navbarEnableUnderlineEffect').checked = config.enable_underline_effect !== undefined ? config.enable_underline_effect : true;
|
||||
document.getElementById('navbarEnableHoverBackground').checked = config.enable_hover_background !== undefined ? config.enable_hover_background : true;
|
||||
|
||||
if (config.custom_styles) {
|
||||
document.getElementById('navbarBoxShadowIntensity').value = config.custom_styles.box_shadow_intensity || 'normal';
|
||||
document.getElementById('navbarBorderRadius').value = config.custom_styles.border_radius !== undefined ? config.custom_styles.border_radius : 4;
|
||||
}
|
||||
|
||||
// Grupo 5: Spacing
|
||||
if (config.custom_styles) {
|
||||
document.getElementById('navbarPaddingVertical').value = config.custom_styles.padding_vertical !== undefined ? config.custom_styles.padding_vertical : 0.75;
|
||||
document.getElementById('navbarLinkPaddingVertical').value = config.custom_styles.link_padding_vertical !== undefined ? config.custom_styles.link_padding_vertical : 0.5;
|
||||
document.getElementById('navbarLinkPaddingHorizontal').value = config.custom_styles.link_padding_horizontal !== undefined ? config.custom_styles.link_padding_horizontal : 0.65;
|
||||
}
|
||||
|
||||
// Grupo 8: Avanzado
|
||||
if (config.custom_styles) {
|
||||
document.getElementById('navbarZIndex').value = config.custom_styles.z_index !== undefined ? config.custom_styles.z_index : 1030;
|
||||
document.getElementById('navbarTransitionSpeed').value = config.custom_styles.transition_speed || 'normal';
|
||||
}
|
||||
|
||||
// Grupo 6: Let's Talk Button
|
||||
if (config.lets_talk_button) {
|
||||
document.getElementById('navbarLetsTalkEnabled').checked = config.lets_talk_button.enabled !== undefined ? config.lets_talk_button.enabled : true;
|
||||
document.getElementById('navbarLetsTalkText').value = config.lets_talk_button.text || "Let's Talk";
|
||||
document.getElementById('navbarLetsTalkIconClass').value = config.lets_talk_button.icon_class || 'bi bi-lightning-charge-fill';
|
||||
document.getElementById('navbarLetsTalkShowIcon').checked = config.lets_talk_button.show_icon !== undefined ? config.lets_talk_button.show_icon : true;
|
||||
document.getElementById('navbarLetsTalkPosition').value = config.lets_talk_button.position || 'right';
|
||||
}
|
||||
|
||||
// Grupo 7: Dropdown
|
||||
if (config.dropdown) {
|
||||
document.getElementById('navbarDropdownEnableHoverDesktop').checked = config.dropdown.enable_hover_desktop !== undefined ? config.dropdown.enable_hover_desktop : true;
|
||||
document.getElementById('navbarDropdownMaxHeight').value = config.dropdown.max_height !== undefined ? config.dropdown.max_height : 70;
|
||||
document.getElementById('navbarDropdownBorderRadius').value = config.dropdown.border_radius !== undefined ? config.dropdown.border_radius : 8;
|
||||
document.getElementById('navbarDropdownItemPaddingVertical').value = config.dropdown.item_padding_vertical !== undefined ? config.dropdown.item_padding_vertical : 0.5;
|
||||
document.getElementById('navbarDropdownItemPaddingHorizontal').value = config.dropdown.item_padding_horizontal !== undefined ? config.dropdown.item_padding_horizontal : 1.25;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,440 +0,0 @@
|
||||
/**
|
||||
* Theme Options Admin JavaScript
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
var ApusThemeOptions = {
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
init: function() {
|
||||
this.tabs();
|
||||
this.imageUpload();
|
||||
this.resetOptions();
|
||||
this.exportOptions();
|
||||
this.importOptions();
|
||||
this.formValidation();
|
||||
this.conditionalFields();
|
||||
},
|
||||
|
||||
/**
|
||||
* Tab Navigation
|
||||
*/
|
||||
tabs: function() {
|
||||
// Tab click handler
|
||||
$('.apus-tabs-nav a').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var tabId = $(this).attr('href');
|
||||
|
||||
// Update active states
|
||||
$('.apus-tabs-nav li').removeClass('active');
|
||||
$(this).parent().addClass('active');
|
||||
|
||||
// Show/hide tab content
|
||||
$('.apus-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) {
|
||||
$('.apus-tabs-nav a[href="' + hash + '"]').trigger('click');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
$(window).on('hashchange', function() {
|
||||
if (window.location.hash) {
|
||||
$('.apus-tabs-nav a[href="' + window.location.hash + '"]').trigger('click');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Image Upload
|
||||
*/
|
||||
imageUpload: function() {
|
||||
var self = this;
|
||||
var mediaUploader;
|
||||
|
||||
// Upload button click
|
||||
$(document).on('click', '.apus-upload-image', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
var container = button.closest('.apus-image-upload');
|
||||
var preview = container.find('.apus-image-preview');
|
||||
var input = container.find('.apus-image-id');
|
||||
var removeBtn = container.find('.apus-remove-image');
|
||||
|
||||
// If the media uploader already exists, reopen it
|
||||
if (mediaUploader) {
|
||||
mediaUploader.open();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new media uploader
|
||||
mediaUploader = wp.media({
|
||||
title: apusAdminOptions.strings.selectImage,
|
||||
button: {
|
||||
text: apusAdminOptions.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="apus-preview-image" />');
|
||||
|
||||
// Show remove button
|
||||
removeBtn.show();
|
||||
});
|
||||
|
||||
// Open the uploader
|
||||
mediaUploader.open();
|
||||
});
|
||||
|
||||
// Remove button click
|
||||
$(document).on('click', '.apus-remove-image', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
var container = button.closest('.apus-image-upload');
|
||||
var preview = container.find('.apus-image-preview');
|
||||
var input = container.find('.apus-image-id');
|
||||
|
||||
// Clear values
|
||||
input.val('');
|
||||
preview.empty();
|
||||
button.hide();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset Options
|
||||
*/
|
||||
resetOptions: function() {
|
||||
$('#apus-reset-options').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm(apusAdminOptions.strings.confirmReset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var button = $(this);
|
||||
button.prop('disabled', true).addClass('updating-message');
|
||||
|
||||
$.ajax({
|
||||
url: apusAdminOptions.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'apus_reset_options',
|
||||
nonce: apusAdminOptions.nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Show success message
|
||||
ApusThemeOptions.showNotice('success', response.data.message);
|
||||
|
||||
// Reload page after 1 second
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
ApusThemeOptions.showNotice('error', response.data.message);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
ApusThemeOptions.showNotice('error', apusAdminOptions.strings.error);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Export Options
|
||||
*/
|
||||
exportOptions: function() {
|
||||
$('#apus-export-options').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var button = $(this);
|
||||
button.prop('disabled', true).addClass('updating-message');
|
||||
|
||||
$.ajax({
|
||||
url: apusAdminOptions.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'apus_export_options',
|
||||
nonce: apusAdminOptions.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);
|
||||
|
||||
ApusThemeOptions.showNotice('success', 'Options exported successfully!');
|
||||
} else {
|
||||
ApusThemeOptions.showNotice('error', response.data.message);
|
||||
}
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
},
|
||||
error: function() {
|
||||
ApusThemeOptions.showNotice('error', apusAdminOptions.strings.error);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Import Options
|
||||
*/
|
||||
importOptions: function() {
|
||||
var modal = $('#apus-import-modal');
|
||||
var importData = $('#apus-import-data');
|
||||
|
||||
// Show modal
|
||||
$('#apus-import-options').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Close modal
|
||||
$('.apus-modal-close, #apus-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
|
||||
$('#apus-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: apusAdminOptions.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'apus_import_options',
|
||||
nonce: apusAdminOptions.nonce,
|
||||
import_data: data
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
ApusThemeOptions.showNotice('success', response.data.message);
|
||||
modal.hide();
|
||||
importData.val('');
|
||||
|
||||
// Reload page after 1 second
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
ApusThemeOptions.showNotice('error', response.data.message);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
ApusThemeOptions.showNotice('error', apusAdminOptions.strings.error);
|
||||
button.prop('disabled', false).removeClass('updating-message');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Form Validation
|
||||
*/
|
||||
formValidation: function() {
|
||||
$('.apus-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 && !ApusThemeOptions.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();
|
||||
}
|
||||
|
||||
ApusThemeOptions.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
|
||||
$('.apus-options-form input, .apus-options-form select, .apus-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('apus-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('apus-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>');
|
||||
|
||||
$('.apus-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() {
|
||||
ApusThemeOptions.init();
|
||||
});
|
||||
|
||||
// Make it globally accessible
|
||||
window.ApusThemeOptions = ApusThemeOptions;
|
||||
|
||||
})(jQuery);
|
||||
@@ -1,604 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Panel - Hero Section Component
|
||||
*
|
||||
* Tab panel para configurar el Hero Section del tema
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: HERO SECTION CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade" id="heroSectionTab" role="tabpanel" aria-labelledby="heroSection-tab">
|
||||
|
||||
<!-- ========================================
|
||||
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-image me-2" style="color: #FF8600;"></i>
|
||||
Configuración del Hero Section
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza el banner principal con título y categorías
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetHeroSectionDefaults">
|
||||
<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">
|
||||
<!-- ========================================
|
||||
COLUMNA IZQUIERDA
|
||||
======================================== -->
|
||||
<div class="col-lg-6">
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
|
||||
======================================== -->
|
||||
<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>
|
||||
|
||||
<!-- Switch 1: Enabled (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="heroSectionEnabled" checked>
|
||||
<label class="form-check-label small" for="heroSectionEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar Hero Section</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="heroSectionShowOnMobile" checked>
|
||||
<label class="form-check-label small" for="heroSectionShowOnMobile" 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-0">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="heroSectionShowOnDesktop" checked>
|
||||
<label class="form-check-label small" for="heroSectionShowOnDesktop" 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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: CONTENIDO 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-card-text me-2" style="color: #FF8600;"></i>
|
||||
Contenido y Estructura
|
||||
</h5>
|
||||
|
||||
<!-- Switch: Show Category Badges -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="heroSectionShowCategoryBadges" checked>
|
||||
<label class="form-check-label small" for="heroSectionShowCategoryBadges" style="color: #495057;">
|
||||
<i class="bi bi-folder me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar Category Badges</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text input: Badge Icon -->
|
||||
<div class="mb-2">
|
||||
<label for="heroSectionCategoryBadgeIcon" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-stars me-1" style="color: #FF8600;"></i>
|
||||
Icono de Category Badge
|
||||
</label>
|
||||
<input type="text" id="heroSectionCategoryBadgeIcon" class="form-control form-control-sm" value="bi bi-folder-fill" placeholder="bi bi-...">
|
||||
<small class="text-muted">Clase de Bootstrap Icons</small>
|
||||
</div>
|
||||
|
||||
<!-- Textarea: Excluded Categories -->
|
||||
<div class="mb-2">
|
||||
<label for="heroSectionExcludedCategories" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-x-circle me-1" style="color: #FF8600;"></i>
|
||||
Categorías Excluidas
|
||||
</label>
|
||||
<textarea id="heroSectionExcludedCategories" class="form-control form-control-sm" rows="2" placeholder="Una por línea">Uncategorized
|
||||
Sin categoría</textarea>
|
||||
<small class="text-muted">Una categoría por línea</small>
|
||||
</div>
|
||||
|
||||
<!-- Compacted row: Alignment + Display Class -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionTitleAlignment" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-text-center me-1" style="color: #FF8600;"></i>
|
||||
Alineación Título
|
||||
</label>
|
||||
<select id="heroSectionTitleAlignment" class="form-select form-select-sm">
|
||||
<option value="left">Izquierda</option>
|
||||
<option value="center" selected>Centro</option>
|
||||
<option value="right">Derecha</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionTitleDisplayClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type-h1 me-1" style="color: #FF8600;"></i>
|
||||
Clase Display
|
||||
</label>
|
||||
<select id="heroSectionTitleDisplayClass" class="form-select form-select-sm">
|
||||
<option value="display-1">display-1</option>
|
||||
<option value="display-2">display-2</option>
|
||||
<option value="display-3">display-3</option>
|
||||
<option value="display-4">display-4</option>
|
||||
<option value="display-5" selected>display-5</option>
|
||||
<option value="display-6">display-6</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 3: COLORES DEL HERO
|
||||
======================================== -->
|
||||
<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>
|
||||
Colores del Hero
|
||||
</h5>
|
||||
|
||||
<!-- Switch: Use Gradient Background -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="heroSectionUseGradientBackground" checked>
|
||||
<label class="form-check-label small" for="heroSectionUseGradientBackground" style="color: #495057;">
|
||||
<i class="bi bi-palette-fill me-1" style="color: #FF8600;"></i>
|
||||
<strong>Usar Gradiente de Fondo</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color pickers: Grid 2x2 (primera fila) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionGradientStartColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Color Gradiente Inicio
|
||||
</label>
|
||||
<input type="color" id="heroSectionGradientStartColor" class="form-control form-control-color w-100" value="#1e3a5f" title="Seleccionar color gradiente inicio">
|
||||
<small class="text-muted d-block mt-1" id="heroSectionGradientStartColorValue">#1E3A5F</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionGradientEndColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket-fill me-1" style="color: #FF8600;"></i>
|
||||
Color Gradiente Fin
|
||||
</label>
|
||||
<input type="color" id="heroSectionGradientEndColor" class="form-control form-control-color w-100" value="#2c5282" title="Seleccionar color gradiente fin">
|
||||
<small class="text-muted d-block mt-1" id="heroSectionGradientEndColorValue">#2C5282</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color pickers: Grid 2x2 (segunda fila) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionHeroTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color Texto H1
|
||||
</label>
|
||||
<input type="color" id="heroSectionHeroTextColor" class="form-control form-control-color w-100" value="#ffffff" title="Seleccionar color texto">
|
||||
<small class="text-muted d-block mt-1" id="heroSectionHeroTextColorValue">#ffffff</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionSolidBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-square-fill me-1" style="color: #FF8600;"></i>
|
||||
Color Fondo Sólido
|
||||
</label>
|
||||
<input type="color" id="heroSectionSolidBackgroundColor" class="form-control form-control-color w-100" value="#1e3a5f" title="Seleccionar color fondo sólido">
|
||||
<small class="text-muted d-block mt-1" id="heroSectionSolidBackgroundColorValue">#1E3A5F</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Range: Gradient Angle -->
|
||||
<div class="mb-0">
|
||||
<label for="heroSectionGradientAngle" class="form-label small mb-1 fw-semibold d-flex justify-content-between align-items-center" style="color: #495057;">
|
||||
<span>
|
||||
<i class="bi bi-arrow-clockwise me-1" style="color: #FF8600;"></i>
|
||||
Ángulo del Gradiente
|
||||
</span>
|
||||
<span class="badge bg-secondary" id="heroSectionGradientAngleValue">135°</span>
|
||||
</label>
|
||||
<input type="range" id="heroSectionGradientAngle" class="form-range" min="0" max="360" step="1" value="135">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: COLORES DE CATEGORY BADGES
|
||||
======================================== -->
|
||||
<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-folder-fill me-2" style="color: #FF8600;"></i>
|
||||
Colores de Category Badges
|
||||
</h5>
|
||||
|
||||
<!-- Color pickers: Grid 2x2 (primera fila) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgeBgColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Background Badge
|
||||
</label>
|
||||
<input type="text" id="heroSectionBadgeBgColor" class="form-control form-control-sm" value="rgba(255, 255, 255, 0.15)" placeholder="rgba(...)">
|
||||
<small class="text-muted">Soporta RGBA</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgeBgHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Background Hover
|
||||
</label>
|
||||
<input type="text" id="heroSectionBadgeBgHoverColor" class="form-control form-control-sm" value="rgba(255, 133, 0, 0.2)" placeholder="rgba(...)">
|
||||
<small class="text-muted">Soporta RGBA</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color pickers: Grid 2x2 (segunda fila) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgeBorderColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-border me-1" style="color: #FF8600;"></i>
|
||||
Border Badge
|
||||
</label>
|
||||
<input type="text" id="heroSectionBadgeBorderColor" class="form-control form-control-sm" value="rgba(255, 255, 255, 0.2)" placeholder="rgba(...)">
|
||||
<small class="text-muted">Soporta RGBA</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgeTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Texto Badge
|
||||
</label>
|
||||
<input type="text" id="heroSectionBadgeTextColor" class="form-control form-control-sm" value="rgba(255, 255, 255, 0.95)" placeholder="rgba(...)">
|
||||
<small class="text-muted">Soporta RGBA</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color picker: Icon Color (full width) -->
|
||||
<div class="mb-0">
|
||||
<label for="heroSectionBadgeIconColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-stars me-1" style="color: #FF8600;"></i>
|
||||
Color Icono Badge
|
||||
</label>
|
||||
<input type="color" id="heroSectionBadgeIconColor" class="form-control form-control-color w-100" value="#FFB800" title="Seleccionar color icono">
|
||||
<small class="text-muted d-block mt-1" id="heroSectionBadgeIconColorValue">#FFB800</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
COLUMNA DERECHA
|
||||
======================================== -->
|
||||
<div class="col-lg-6">
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 5: ESPACIADO Y DIMENSIONES
|
||||
======================================== -->
|
||||
<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>
|
||||
Espaciado y Dimensiones
|
||||
</h5>
|
||||
|
||||
<!-- Hero padding compactado -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionHeroPaddingVertical" 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 (rem)
|
||||
</label>
|
||||
<input type="number" id="heroSectionHeroPaddingVertical" class="form-control form-control-sm" value="3" min="0" max="10" step="0.5">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionHeroPaddingHorizontal" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-expand me-1" style="color: #FF8600;"></i>
|
||||
Padding Horizontal (rem)
|
||||
</label>
|
||||
<input type="number" id="heroSectionHeroPaddingHorizontal" class="form-control form-control-sm" value="0" min="0" max="10" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Margin + Gap compactado -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionHeroMarginBottom" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrow-down me-1" style="color: #FF8600;"></i>
|
||||
Margin Bottom (rem)
|
||||
</label>
|
||||
<input type="number" id="heroSectionHeroMarginBottom" class="form-control form-control-sm" value="1.5" min="0" max="5" step="0.5">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgesGap" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-distribute-horizontal me-1" style="color: #FF8600;"></i>
|
||||
Gap Badges (rem)
|
||||
</label>
|
||||
<input type="number" id="heroSectionBadgesGap" class="form-control form-control-sm" value="0.5" min="0" max="3" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badge padding compactado -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgePaddingVertical" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
|
||||
Badge Padding V (rem)
|
||||
</label>
|
||||
<input type="number" id="heroSectionBadgePaddingVertical" class="form-control form-control-sm" value="0.375" min="0" max="2" step="0.125">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgePaddingHorizontal" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-expand me-1" style="color: #FF8600;"></i>
|
||||
Badge Padding H (rem)
|
||||
</label>
|
||||
<input type="number" id="heroSectionBadgePaddingHorizontal" class="form-control form-control-sm" value="0.875" min="0" max="2" step="0.125">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Border radius -->
|
||||
<div class="mb-0">
|
||||
<label for="heroSectionBadgeBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-diamond me-1" style="color: #FF8600;"></i>
|
||||
Border Radius Badge (px)
|
||||
</label>
|
||||
<input type="number" id="heroSectionBadgeBorderRadius" class="form-control form-control-sm" value="20" min="0" max="50" step="1">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 6: TIPOGRAFÍA
|
||||
======================================== -->
|
||||
<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-type me-2" style="color: #FF8600;"></i>
|
||||
Tipografía
|
||||
</h5>
|
||||
|
||||
<!-- H1 Font Weight + Badge Font Size compactado -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionH1FontWeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type-bold me-1" style="color: #FF8600;"></i>
|
||||
Font Weight H1
|
||||
</label>
|
||||
<select id="heroSectionH1FontWeight" class="form-select form-select-sm">
|
||||
<option value="400">400 (Normal)</option>
|
||||
<option value="500">500 (Medium)</option>
|
||||
<option value="600">600 (Semibold)</option>
|
||||
<option value="700" selected>700 (Bold)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgeFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Badge Font Size (rem)
|
||||
</label>
|
||||
<input type="number" id="heroSectionBadgeFontSize" class="form-control form-control-sm" value="0.813" min="0.5" max="2" step="0.125">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badge Font Weight + H1 Line Height compactado -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgeFontWeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type-bold me-1" style="color: #FF8600;"></i>
|
||||
Font Weight Badge
|
||||
</label>
|
||||
<select id="heroSectionBadgeFontWeight" class="form-select form-select-sm">
|
||||
<option value="400">400 (Normal)</option>
|
||||
<option value="500" selected>500 (Medium)</option>
|
||||
<option value="600">600 (Semibold)</option>
|
||||
<option value="700">700 (Bold)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionH1LineHeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-text-paragraph me-1" style="color: #FF8600;"></i>
|
||||
Line Height H1
|
||||
</label>
|
||||
<input type="number" id="heroSectionH1LineHeight" class="form-control form-control-sm" value="1.4" min="1" max="3" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 7: EFECTOS VISUALES
|
||||
======================================== -->
|
||||
<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-stars me-2" style="color: #FF8600;"></i>
|
||||
Efectos Visuales
|
||||
</h5>
|
||||
|
||||
<!-- Switch: Enable H1 Text Shadow -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="heroSectionEnableH1TextShadow" checked>
|
||||
<label class="form-check-label small" for="heroSectionEnableH1TextShadow" style="color: #495057;">
|
||||
<i class="bi bi-sun me-1" style="color: #FF8600;"></i>
|
||||
<strong>Habilitar Text Shadow H1</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text input: H1 Text Shadow -->
|
||||
<div class="mb-2">
|
||||
<label for="heroSectionH1TextShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
|
||||
Text Shadow H1
|
||||
</label>
|
||||
<input type="text" id="heroSectionH1TextShadow" class="form-control form-control-sm" value="1px 1px 2px rgba(0, 0, 0, 0.2)" placeholder="CSS shadow">
|
||||
<small class="text-muted">Sintaxis CSS: x y blur color</small>
|
||||
</div>
|
||||
|
||||
<!-- Switch: Enable Hero Box Shadow -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="heroSectionEnableHeroBoxShadow" checked>
|
||||
<label class="form-check-label small" for="heroSectionEnableHeroBoxShadow" style="color: #495057;">
|
||||
<i class="bi bi-box me-1" style="color: #FF8600;"></i>
|
||||
<strong>Habilitar Box Shadow Hero</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text input: Hero Box Shadow -->
|
||||
<div class="mb-2">
|
||||
<label for="heroSectionHeroBoxShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-box-seam me-1" style="color: #FF8600;"></i>
|
||||
Box Shadow Hero
|
||||
</label>
|
||||
<input type="text" id="heroSectionHeroBoxShadow" class="form-control form-control-sm" value="0 4px 16px rgba(30, 58, 95, 0.25)" placeholder="CSS shadow">
|
||||
<small class="text-muted">Sintaxis CSS: x y blur spread color</small>
|
||||
</div>
|
||||
|
||||
<!-- Switch: Enable Badge Backdrop Filter -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="heroSectionEnableBadgeBackdropFilter" checked>
|
||||
<label class="form-check-label small" for="heroSectionEnableBadgeBackdropFilter" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
<strong>Habilitar Backdrop Filter Badge</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text input: Badge Backdrop Filter -->
|
||||
<div class="mb-0">
|
||||
<label for="heroSectionBadgeBackdropFilter" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-filter me-1" style="color: #FF8600;"></i>
|
||||
Backdrop Filter Badge
|
||||
</label>
|
||||
<input type="text" id="heroSectionBadgeBackdropFilter" class="form-control form-control-sm" value="blur(10px)" placeholder="CSS filter">
|
||||
<small class="text-muted">Ej: blur(10px), brightness(1.2)</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 8: TRANSICIONES Y ANIMACIONES
|
||||
======================================== -->
|
||||
<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-lightning me-2" style="color: #FF8600;"></i>
|
||||
Transiciones y Animaciones
|
||||
</h5>
|
||||
|
||||
<!-- Compacted row: Transition Speed + Hover Effect -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgeTransitionSpeed" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-speedometer me-1" style="color: #FF8600;"></i>
|
||||
Velocidad Transición
|
||||
</label>
|
||||
<select id="heroSectionBadgeTransitionSpeed" class="form-select form-select-sm">
|
||||
<option value="fast">Rápida (0.15s)</option>
|
||||
<option value="normal" selected>Normal (0.3s)</option>
|
||||
<option value="slow">Lenta (0.5s)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="heroSectionBadgeHoverEffect" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Efecto Hover
|
||||
</label>
|
||||
<select id="heroSectionBadgeHoverEffect" class="form-select form-select-sm">
|
||||
<option value="none">Ninguno</option>
|
||||
<option value="background" selected>Background</option>
|
||||
<option value="scale">Escala</option>
|
||||
<option value="brightness">Brillo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 9: AVANZADO
|
||||
======================================== -->
|
||||
<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-code-slash me-2" style="color: #FF8600;"></i>
|
||||
Avanzado
|
||||
</h5>
|
||||
|
||||
<!-- Text input: Custom Hero Classes -->
|
||||
<div class="mb-2">
|
||||
<label for="heroSectionCustomHeroClasses" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-braces me-1" style="color: #FF8600;"></i>
|
||||
Custom CSS Classes Hero
|
||||
</label>
|
||||
<input type="text" id="heroSectionCustomHeroClasses" class="form-control form-control-sm" value="" placeholder="custom-class-1 custom-class-2">
|
||||
<small class="text-muted">Clases CSS adicionales separadas por espacio</small>
|
||||
</div>
|
||||
|
||||
<!-- Text input: Custom Badge Classes -->
|
||||
<div class="mb-0">
|
||||
<label for="heroSectionCustomBadgeClasses" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-braces-asterisk me-1" style="color: #FF8600;"></i>
|
||||
Custom CSS Classes Badge
|
||||
</label>
|
||||
<input type="text" id="heroSectionCustomBadgeClasses" class="form-control form-control-sm" value="" placeholder="badge-custom-1">
|
||||
<small class="text-muted">Clases CSS adicionales para badges</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,393 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Component: Let's Talk Button Configuration
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: BOTÓN LET'S TALK CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade" id="letsTalkButtonTab" role="tabpanel" aria-labelledby="lets-talk-button-config-tab">
|
||||
|
||||
<!-- ========================================
|
||||
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-lightning-charge-fill me-2" style="color: #FF8600;"></i>
|
||||
Configuración del Botón Let's Talk
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza el botón de contacto "Let's Talk" del navbar
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetLetsTalkButtonDefaults">
|
||||
<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 (3 campos)
|
||||
PATRÓN 3: CARD CON BORDER-LEFT NAVY
|
||||
PATRÓN 4: 3 SWITCHES OBLIGATORIOS
|
||||
======================================== -->
|
||||
<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>
|
||||
|
||||
<!-- Switch 1: Enabled (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="letsTalkButtonEnabled" checked>
|
||||
<label class="form-check-label small" for="letsTalkButtonEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar Botón Let's Talk</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="letsTalkButtonShowOnMobile" checked>
|
||||
<label class="form-check-label small" for="letsTalkButtonShowOnMobile" 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-0">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="letsTalkButtonShowOnDesktop" checked>
|
||||
<label class="form-check-label small" for="letsTalkButtonShowOnDesktop" 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: CONTENIDO (3 campos)
|
||||
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-chat-text me-2" style="color: #FF8600;"></i>
|
||||
Contenido
|
||||
</h5>
|
||||
|
||||
<!-- Switch: show_icon -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="letsTalkButtonShowIcon" checked>
|
||||
<label class="form-check-label small" for="letsTalkButtonShowIcon" style="color: #495057;">
|
||||
<i class="bi bi-eye me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar icono</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text inputs compactados: text + icon_class -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Texto del botón
|
||||
</label>
|
||||
<input type="text" id="letsTalkButtonText" class="form-control form-control-sm" value="Let's Talk" maxlength="30">
|
||||
<small class="text-muted">Máximo 30 caracteres</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonIconClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star me-1" style="color: #FF8600;"></i>
|
||||
Clase del icono
|
||||
</label>
|
||||
<input type="text" id="letsTalkButtonIconClass" class="form-control form-control-sm" value="bi bi-lightning-charge-fill" placeholder="bi bi-...">
|
||||
<small class="text-muted">Bootstrap Icons</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 3: TIPOGRAFÍA (1 campo)
|
||||
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-fonts me-2" style="color: #FF8600;"></i>
|
||||
Tipografía
|
||||
</h5>
|
||||
|
||||
<!-- Select: font_weight -->
|
||||
<div class="mb-0">
|
||||
<label for="letsTalkButtonFontWeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type-bold me-1" style="color: #FF8600;"></i>
|
||||
Peso de fuente
|
||||
</label>
|
||||
<select id="letsTalkButtonFontWeight" class="form-select form-select-sm">
|
||||
<option value="400">Normal (400)</option>
|
||||
<option value="500">Medium (500)</option>
|
||||
<option value="600" selected>Semibold (600)</option>
|
||||
<option value="700">Bold (700)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: COMPORTAMIENTO (1 campo)
|
||||
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-gear me-2" style="color: #FF8600;"></i>
|
||||
Comportamiento
|
||||
</h5>
|
||||
|
||||
<!-- Text input: modal_target -->
|
||||
<div class="mb-0">
|
||||
<label for="letsTalkButtonModalTarget" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-window me-1" style="color: #FF8600;"></i>
|
||||
ID del modal
|
||||
</label>
|
||||
<input type="text" id="letsTalkButtonModalTarget" class="form-control form-control-sm" value="#contactModal" maxlength="50" placeholder="#nombreModal">
|
||||
<small class="text-muted">Debe comenzar con #</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 5: ESPACIADO Y POSICIÓN (3 campos)
|
||||
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-arrows-angle-expand me-2" style="color: #FF8600;"></i>
|
||||
Espaciado y Posición
|
||||
</h5>
|
||||
|
||||
<!-- Number inputs compactados: padding_vertical + padding_horizontal -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonPaddingVertical" 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="number" id="letsTalkButtonPaddingVertical" class="form-control form-control-sm" value="0.5" min="0" max="3" step="0.1">
|
||||
<small class="text-muted">En rem (0-3)</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonPaddingHorizontal" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-horizontal me-1" style="color: #FF8600;"></i>
|
||||
Padding horizontal
|
||||
</label>
|
||||
<input type="number" id="letsTalkButtonPaddingHorizontal" class="form-control form-control-sm" value="1.5" min="0" max="5" step="0.1">
|
||||
<small class="text-muted">En rem (0-5)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select: position -->
|
||||
<div class="mb-0">
|
||||
<label for="letsTalkButtonPosition" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-pin-angle me-1" style="color: #FF8600;"></i>
|
||||
Posición en navbar
|
||||
</label>
|
||||
<select id="letsTalkButtonPosition" class="form-select form-select-sm">
|
||||
<option value="left">Izquierda</option>
|
||||
<option value="center">Centro</option>
|
||||
<option value="right" selected>Derecha</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 6: COLORES PERSONALIZADOS (4 campos)
|
||||
PATRÓN 3: CARD CON BORDER-LEFT NAVY
|
||||
PATRÓN 5: COLOR PICKERS EN GRID 2X2
|
||||
======================================== -->
|
||||
<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>
|
||||
Colores Personalizados
|
||||
</h5>
|
||||
|
||||
<!-- Color pickers en grid 2x2 -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonBgColor" 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="letsTalkButtonBgColor" class="form-control form-control-color w-100" value="#FF8600" title="Seleccionar color de fondo">
|
||||
<small class="text-muted d-block mt-1" id="letsTalkButtonBgColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonBgHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-cursor me-1" style="color: #FF8600;"></i>
|
||||
Color hover
|
||||
</label>
|
||||
<input type="color" id="letsTalkButtonBgHoverColor" class="form-control form-control-color w-100" value="#FF6B35" title="Seleccionar color hover">
|
||||
<small class="text-muted d-block mt-1" id="letsTalkButtonBgHoverColorValue">#FF6B35</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Color de texto
|
||||
</label>
|
||||
<input type="color" id="letsTalkButtonTextColor" class="form-control form-control-color w-100" value="#ffffff" title="Seleccionar color de texto">
|
||||
<small class="text-muted d-block mt-1" id="letsTalkButtonTextColorValue">#ffffff</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonIconColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>
|
||||
Color icono
|
||||
</label>
|
||||
<input type="color" id="letsTalkButtonIconColor" class="form-control form-control-color w-100" value="#ffffff" title="Seleccionar color icono">
|
||||
<small class="text-muted d-block mt-1" id="letsTalkButtonIconColorValue">#ffffff</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 7: ESTILOS AVANZADOS (8 campos)
|
||||
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-sliders me-2" style="color: #FF8600;"></i>
|
||||
Estilos Avanzados
|
||||
</h5>
|
||||
|
||||
<!-- Number inputs compactados: border_radius + border_width -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-border-radius me-1" style="color: #FF8600;"></i>
|
||||
Radio esquinas
|
||||
</label>
|
||||
<input type="number" id="letsTalkButtonBorderRadius" class="form-control form-control-sm" value="6" min="0" max="30" step="1">
|
||||
<small class="text-muted">En px (0-30)</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonBorderWidth" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-border-width me-1" style="color: #FF8600;"></i>
|
||||
Ancho de borde
|
||||
</label>
|
||||
<input type="number" id="letsTalkButtonBorderWidth" class="form-control form-control-sm" value="0" min="0" max="10" step="1">
|
||||
<small class="text-muted">En px (0-10)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Border color + border style compactados -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonBorderColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-border-style me-1" style="color: #FF8600;"></i>
|
||||
Color borde
|
||||
</label>
|
||||
<input type="color" id="letsTalkButtonBorderColor" class="form-control form-control-color w-100" value="#000000" title="Seleccionar color borde">
|
||||
<small class="text-muted d-block mt-1" id="letsTalkButtonBorderColorValue">#000000</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonBorderStyle" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-dash-lg me-1" style="color: #FF8600;"></i>
|
||||
Estilo borde
|
||||
</label>
|
||||
<select id="letsTalkButtonBorderStyle" class="form-select form-select-sm">
|
||||
<option value="solid" selected>Sólido</option>
|
||||
<option value="dashed">Guiones</option>
|
||||
<option value="dotted">Puntos</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch: enable_box_shadow -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="letsTalkButtonEnableBoxShadow">
|
||||
<label class="form-check-label small" for="letsTalkButtonEnableBoxShadow" style="color: #495057;">
|
||||
<i class="bi bi-box-seam me-1" style="color: #FF8600;"></i>
|
||||
<strong>Habilitar sombra</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text input: box_shadow -->
|
||||
<div class="mb-2">
|
||||
<label for="letsTalkButtonBoxShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-shadow me-1" style="color: #FF8600;"></i>
|
||||
CSS box-shadow
|
||||
</label>
|
||||
<input type="text" id="letsTalkButtonBoxShadow" class="form-control form-control-sm" value="0 2px 8px rgba(0, 0, 0, 0.15)" maxlength="100">
|
||||
<small class="text-muted">Ejemplo: 0 4px 12px rgba(255, 134, 0, 0.3)</small>
|
||||
</div>
|
||||
|
||||
<!-- Selects compactados: transition_speed + hover_effect -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonTransitionSpeed" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-speedometer me-1" style="color: #FF8600;"></i>
|
||||
Velocidad
|
||||
</label>
|
||||
<select id="letsTalkButtonTransitionSpeed" class="form-select form-select-sm">
|
||||
<option value="fast">Rápido (0.2s)</option>
|
||||
<option value="normal" selected>Normal (0.3s)</option>
|
||||
<option value="slow">Lento (0.5s)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="letsTalkButtonHoverEffect" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-magic me-1" style="color: #FF8600;"></i>
|
||||
Efecto hover
|
||||
</label>
|
||||
<select id="letsTalkButtonHoverEffect" class="form-select form-select-sm">
|
||||
<option value="none" selected>Ninguno</option>
|
||||
<option value="scale">Escala (1.05)</option>
|
||||
<option value="brightness">Brillo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,585 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Navbar Component - Admin Interface v2.0
|
||||
* Sigue los 4 patrones obligatorios de Top Bar
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: NAVBAR CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade"
|
||||
id="navbarTab"
|
||||
role="tabpanel"
|
||||
aria-labelledby="navbar-config-tab">
|
||||
|
||||
<!-- ========================================
|
||||
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 me-2" style="color: #FF8600;"></i>
|
||||
Configuración Navbar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza el menú de navegación principal de tu 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">
|
||||
|
||||
<!-- ========================================
|
||||
COLUMNA IZQUIERDA
|
||||
======================================== -->
|
||||
<div class="col-lg-6">
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD
|
||||
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 -->
|
||||
|
||||
<!-- Enabled -->
|
||||
<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>
|
||||
|
||||
<!-- Show on Mobile -->
|
||||
<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>
|
||||
|
||||
<!-- Show on Desktop -->
|
||||
<div class="mb-0">
|
||||
<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>
|
||||
|
||||
<!-- Selects compactados en fila -->
|
||||
<div class="row g-2 mt-3">
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarPosition" class="text-secondary fw-medium mb-1">
|
||||
Posición
|
||||
</label>
|
||||
<select id="navbarPosition" class="form-select form-select-sm">
|
||||
<option value="sticky" selected>Sticky (fija al scroll)</option>
|
||||
<option value="static">Static (normal)</option>
|
||||
<option value="fixed">Fixed (siempre fija)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarBreakpoint" class="text-secondary fw-medium mb-1">
|
||||
Breakpoint
|
||||
</label>
|
||||
<select id="navbarBreakpoint" class="form-select form-select-sm">
|
||||
<option value="sm">SM (576px)</option>
|
||||
<option value="md">MD (768px)</option>
|
||||
<option value="lg" selected>LG (992px)</option>
|
||||
<option value="xl">XL (1200px)</option>
|
||||
<option value="xxl">XXL (1400px)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: COLORES PERSONALIZADOS
|
||||
======================================== -->
|
||||
<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>
|
||||
Colores Personalizados
|
||||
</h5>
|
||||
|
||||
<!-- 7 colores en grid 2x2 (patrón Top Bar) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarBgColor" 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="navbarBgColor" class="form-control form-control-color w-100" value="#1e3a5f" title="Seleccionar color de fondo">
|
||||
<small class="text-muted d-block mt-1" id="navbarBgColorValue">#1e3a5f</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<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 de texto
|
||||
</label>
|
||||
<input type="color" id="navbarTextColor" class="form-control form-control-color w-100" value="#ffffff" title="Seleccionar color de texto">
|
||||
<small class="text-muted d-block mt-1" id="navbarTextColorValue">#ffffff</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarLinkHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-cursor me-1" style="color: #FF8600;"></i>
|
||||
Color hover links
|
||||
</label>
|
||||
<input type="color" id="navbarLinkHoverColor" class="form-control form-control-color w-100" value="#FF8600" title="Seleccionar color hover enlaces">
|
||||
<small class="text-muted d-block mt-1" id="navbarLinkHoverColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarLinkHoverBgColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-square-fill me-1" style="color: #FF8600;"></i>
|
||||
Background hover
|
||||
</label>
|
||||
<input type="color" id="navbarLinkHoverBgColor" class="form-control form-control-color w-100" value="#FF8600" title="Seleccionar color fondo hover">
|
||||
<small class="text-muted d-block mt-1" id="navbarLinkHoverBgColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownBgColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-card-text me-1" style="color: #FF8600;"></i>
|
||||
Dropdown BG
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownBgColor" class="form-control form-control-color w-100" value="#ffffff" title="Seleccionar color fondo dropdown">
|
||||
<small class="text-muted d-block mt-1" id="navbarDropdownBgColorValue">#ffffff</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownItemColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-text-left me-1" style="color: #FF8600;"></i>
|
||||
Item Color
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownItemColor" class="form-control form-control-color w-100" value="#495057" title="Seleccionar color items dropdown">
|
||||
<small class="text-muted d-block mt-1" id="navbarDropdownItemColorValue">#495057</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownItemHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Item Hover
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownItemHoverColor" class="form-control form-control-color w-100" value="#FF8600" title="Seleccionar color hover items dropdown">
|
||||
<small class="text-muted d-block mt-1" id="navbarDropdownItemHoverColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 3: TIPOGRAFÍA
|
||||
======================================== -->
|
||||
<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-fonts me-2" style="color: #FF8600;"></i>
|
||||
Tipografía
|
||||
</h5>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarFontSize" class="text-secondary fw-medium mb-1">
|
||||
Tamaño de fuente
|
||||
</label>
|
||||
<select id="navbarFontSize" class="form-select form-select-sm">
|
||||
<option value="small">Pequeño (0.8rem)</option>
|
||||
<option value="normal" selected>Normal (0.9rem)</option>
|
||||
<option value="large">Grande (1rem)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarFontWeight" class="text-secondary fw-medium mb-1">
|
||||
Peso de fuente
|
||||
</label>
|
||||
<select id="navbarFontWeight" class="form-select form-select-sm">
|
||||
<option value="400">Normal (400)</option>
|
||||
<option value="500" selected>Medium (500)</option>
|
||||
<option value="600">Semibold (600)</option>
|
||||
<option value="700">Bold (700)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: EFECTOS VISUALES
|
||||
======================================== -->
|
||||
<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-magic me-2" style="color: #FF8600;"></i>
|
||||
Efectos Visuales
|
||||
</h5>
|
||||
|
||||
<!-- Switches verticales -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnableBoxShadow" checked="">
|
||||
<label class="form-check-label small" for="navbarEnableBoxShadow" style="color: #495057;">
|
||||
<i class="bi bi-box-arrow-down me-1" style="color: #FF8600;"></i>
|
||||
<strong>Habilitar Box Shadow</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnableUnderlineEffect" checked="">
|
||||
<label class="form-check-label small" for="navbarEnableUnderlineEffect" style="color: #495057;">
|
||||
<i class="bi bi-dash-lg me-1" style="color: #FF8600;"></i>
|
||||
<strong>Línea animada al hover</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnableHoverBackground" checked="">
|
||||
<label class="form-check-label small" for="navbarEnableHoverBackground" style="color: #495057;">
|
||||
<i class="bi bi-square-fill me-1" style="color: #FF8600;"></i>
|
||||
<strong>Background al hover</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selects y números compactados -->
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarBoxShadowIntensity" class="text-secondary fw-medium mb-1">
|
||||
Intensidad sombra
|
||||
</label>
|
||||
<select id="navbarBoxShadowIntensity" class="form-select form-select-sm">
|
||||
<option value="none">Sin sombra</option>
|
||||
<option value="light">Ligera</option>
|
||||
<option value="normal" selected>Normal</option>
|
||||
<option value="strong">Fuerte</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarBorderRadius" class="text-secondary fw-medium mb-1">
|
||||
Border radius (px)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarBorderRadius"
|
||||
class="form-control form-control-sm"
|
||||
value="4"
|
||||
min="0"
|
||||
max="20">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- Fin columna izquierda -->
|
||||
|
||||
<!-- ========================================
|
||||
COLUMNA DERECHA
|
||||
======================================== -->
|
||||
<div class="col-lg-6">
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 5: SPACING
|
||||
======================================== -->
|
||||
<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-expand me-2" style="color: #FF8600;"></i>
|
||||
Espaciado
|
||||
</h5>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label for="navbarPaddingVertical" class="text-secondary fw-medium mb-1">
|
||||
Padding navbar (rem)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarPaddingVertical"
|
||||
class="form-control form-control-sm"
|
||||
value="0.75"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.05">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label for="navbarLinkPaddingVertical" class="text-secondary fw-medium mb-1">
|
||||
Padding links V (rem)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarLinkPaddingVertical"
|
||||
class="form-control form-control-sm"
|
||||
value="0.5"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.05">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label for="navbarLinkPaddingHorizontal" class="text-secondary fw-medium mb-1">
|
||||
Padding links H (rem)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarLinkPaddingHorizontal"
|
||||
class="form-control form-control-sm"
|
||||
value="0.65"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.05">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 6: LET'S TALK BUTTON
|
||||
======================================== -->
|
||||
<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-lightning-charge-fill me-2" style="color: #FF8600;"></i>
|
||||
Botón "Let's Talk"
|
||||
</h5>
|
||||
|
||||
<!-- Switches verticales -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarLetsTalkEnabled" checked="">
|
||||
<label class="form-check-label small" for="navbarLetsTalkEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar botón</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarLetsTalkShowIcon" checked="">
|
||||
<label class="form-check-label small" for="navbarLetsTalkShowIcon" style="color: #495057;">
|
||||
<i class="bi bi-eye me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar icono</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texto e icono -->
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-3">
|
||||
<label for="navbarLetsTalkText" class="text-secondary fw-medium mb-1">
|
||||
Texto del botón
|
||||
</label>
|
||||
<input type="text"
|
||||
id="navbarLetsTalkText"
|
||||
class="form-control form-control-sm"
|
||||
value="Let's Talk"
|
||||
maxlength="30"
|
||||
placeholder="Let's Talk">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-3">
|
||||
<label for="navbarLetsTalkIconClass" class="text-secondary fw-medium mb-1">
|
||||
Clase del icono
|
||||
</label>
|
||||
<input type="text"
|
||||
id="navbarLetsTalkIconClass"
|
||||
class="form-control form-control-sm"
|
||||
value="bi bi-lightning-charge-fill"
|
||||
maxlength="50"
|
||||
placeholder="bi bi-lightning-charge-fill">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posición -->
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarLetsTalkPosition" class="text-secondary fw-medium mb-1">
|
||||
Posición dentro del navbar
|
||||
</label>
|
||||
<select id="navbarLetsTalkPosition" class="form-select form-select-sm">
|
||||
<option value="left">Izquierda</option>
|
||||
<option value="center">Centro</option>
|
||||
<option value="right" selected>Derecha</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 7: 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>
|
||||
Dropdown
|
||||
</h5>
|
||||
|
||||
<!-- Switch vertical -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarDropdownEnableHoverDesktop" checked="">
|
||||
<label class="form-check-label small" for="navbarDropdownEnableHoverDesktop" style="color: #495057;">
|
||||
<i class="bi bi-cursor me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar al hover (desktop)</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Números y selects -->
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-3">
|
||||
<label for="navbarDropdownMaxHeight" class="text-secondary fw-medium mb-1">
|
||||
Altura máxima (vh)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarDropdownMaxHeight"
|
||||
class="form-control form-control-sm"
|
||||
value="70"
|
||||
min="30"
|
||||
max="90">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-3">
|
||||
<label for="navbarDropdownBorderRadius" class="text-secondary fw-medium mb-1">
|
||||
Border radius (px)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarDropdownBorderRadius"
|
||||
class="form-control form-control-sm"
|
||||
value="8"
|
||||
min="0"
|
||||
max="20">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarDropdownItemPaddingVertical" class="text-secondary fw-medium mb-1">
|
||||
Padding items V (rem)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarDropdownItemPaddingVertical"
|
||||
class="form-control form-control-sm"
|
||||
value="0.5"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.05">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarDropdownItemPaddingHorizontal" class="text-secondary fw-medium mb-1">
|
||||
Padding items H (rem)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarDropdownItemPaddingHorizontal"
|
||||
class="form-control form-control-sm"
|
||||
value="1.25"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.05">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 8: AVANZADO (OPCIONAL)
|
||||
======================================== -->
|
||||
<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>
|
||||
Avanzado
|
||||
</h5>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarZIndex" class="text-secondary fw-medium mb-1">
|
||||
Z-index
|
||||
</label>
|
||||
<input type="number"
|
||||
id="navbarZIndex"
|
||||
class="form-control form-control-sm"
|
||||
value="1030"
|
||||
min="0"
|
||||
max="9999">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<label for="navbarTransitionSpeed" class="text-secondary fw-medium mb-1">
|
||||
Velocidad transiciones
|
||||
</label>
|
||||
<select id="navbarTransitionSpeed" class="form-select form-select-sm">
|
||||
<option value="fast">Rápida (0.2s)</option>
|
||||
<option value="normal" selected>Normal (0.3s)</option>
|
||||
<option value="slow">Lenta (0.5s)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- Fin columna derecha -->
|
||||
|
||||
</div> <!-- Fin row g-3 -->
|
||||
|
||||
</div> <!-- Fin tab-pane -->
|
||||
@@ -1,237 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Component: Top Bar Configuration
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @subpackage Admin_Panel
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="tab-pane fade show active" id="topBarTab" role="tabpanel">
|
||||
<!-- Header del Tab -->
|
||||
<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 Top Bar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza la barra de anuncios superior de tu 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>
|
||||
|
||||
<!-- Grid: 2 columnas + 1 fila completa -->
|
||||
<div class="row g-3">
|
||||
<!-- COLUMNA IZQUIERDA -->
|
||||
<div class="col-lg-6">
|
||||
<!-- GRUPO 1: ACTIVACIÓN -->
|
||||
<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>
|
||||
|
||||
<!-- Enabled -->
|
||||
<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 Top Bar</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show on Mobile -->
|
||||
<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>
|
||||
|
||||
<!-- Show on Desktop -->
|
||||
<div class="mb-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GRUPO 2: ESTILOS -->
|
||||
<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 Personalizados
|
||||
</h5>
|
||||
|
||||
<!-- 4 colores en grid 2x2 -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="topBarBgColor" 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="topBarBgColor" class="form-control form-control-color w-100" value="#0E2337" title="Seleccionar color de fondo">
|
||||
<small class="text-muted d-block mt-1" id="topBarBgColorValue">#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="Seleccionar color de texto">
|
||||
<small class="text-muted d-block mt-1" id="topBarTextColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarHighlightColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star me-1" style="color: #FF8600;"></i>
|
||||
Color destacado
|
||||
</label>
|
||||
<input type="color" id="topBarHighlightColor" class="form-control form-control-color w-100" value="#FF8600" title="Seleccionar color destacado">
|
||||
<small class="text-muted d-block mt-1" id="topBarHighlightColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-cursor me-1" style="color: #FF8600;"></i>
|
||||
Hover enlace
|
||||
</label>
|
||||
<input type="color" id="topBarLinkHoverColor" class="form-control form-control-color w-100" value="#FF6B35" title="Seleccionar color hover del enlace">
|
||||
<small class="text-muted d-block mt-1" id="topBarLinkHoverColorValue">#FF6B35</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tamaño de fuente -->
|
||||
<div class="mb-0">
|
||||
<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>
|
||||
<select id="topBarFontSize" class="form-select form-select-sm">
|
||||
<option value="small">Pequeño (0.8rem)</option>
|
||||
<option value="normal" selected="">Normal (0.9rem)</option>
|
||||
<option value="large">Grande (1rem)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COLUMNA DERECHA -->
|
||||
<div class="col-lg-6">
|
||||
<!-- GRUPO 3: 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-card-text me-2" style="color: #FF8600;"></i>
|
||||
Contenido y Mensajes
|
||||
</h5>
|
||||
|
||||
<!-- Icono + mostrar -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-8">
|
||||
<label for="topBarIconClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-emoji-smile me-1" style="color: #FF8600;"></i>
|
||||
Clase del icono <span class="badge bg-secondary" style="font-size: 0.65rem;">Bootstrap Icons</span>
|
||||
</label>
|
||||
<input type="text" id="topBarIconClass" class="form-control form-control-sm" placeholder="bi bi-megaphone-fill" value="bi bi-megaphone-fill" maxlength="50">
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Ver: <a href="https://icons.getbootstrap.com/" target="_blank" class="text-decoration-none" style="color: #FF8600;">Bootstrap Icons <i class="bi bi-box-arrow-up-right"></i></a>
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label small mb-1 fw-semibold" style="color: #495057;">Opciones</label>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="topBarShowIcon" checked="">
|
||||
<label class="form-check-label small" for="topBarShowIcon" style="color: #495057;">Mostrar</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texto destacado -->
|
||||
<div class="mb-2">
|
||||
<label for="topBarHighlightText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bookmark-star me-1" style="color: #FF8600;"></i>
|
||||
Texto destacado <span class="badge text-dark" style="background-color: #FFB800; font-size: 0.65rem;">Opcional</span>
|
||||
</label>
|
||||
<input type="text" id="topBarHighlightText" class="form-control form-control-sm" placeholder="Ej: "Nuevo:" o "Promoción:"" value="Nuevo:" maxlength="30">
|
||||
</div>
|
||||
|
||||
<!-- Mensaje principal -->
|
||||
<div class="mb-2">
|
||||
<label for="topBarMessageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-left-text me-1" style="color: #FF8600;"></i>
|
||||
Mensaje principal <span class="text-danger">*</span>
|
||||
<span class="float-end text-muted"><span id="topBarMessageTextCount" class="fw-bold">77</span>/250</span>
|
||||
</label>
|
||||
<textarea id="topBarMessageText" class="form-control form-control-sm" rows="2" maxlength="250" placeholder="Ej: Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025." required="">Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</textarea>
|
||||
<div class="progress mt-1" style="height: 3px;">
|
||||
<div id="topBarMessageTextProgress" class="progress-bar bg-orange-primary" role="progressbar" style="width: 30.8%; background-color: rgb(255, 134, 0);" aria-valuenow="77" aria-valuemin="0" aria-valuemax="250"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enlace (3 campos compactos) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-5">
|
||||
<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 enlace
|
||||
</label>
|
||||
<input type="text" id="topBarLinkText" class="form-control form-control-sm" placeholder="Ver Catálogo" value="Ver Catálogo →" maxlength="50">
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-globe me-1" style="color: #FF8600;"></i>
|
||||
URL
|
||||
</label>
|
||||
<input type="url" id="topBarLinkUrl" class="form-control form-control-sm" placeholder="/catalogo" value="/catalogo">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label for="topBarLinkTarget" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-window me-1" style="color: #FF8600;"></i>
|
||||
Target
|
||||
</label>
|
||||
<select id="topBarLinkTarget" class="form-select form-select-sm">
|
||||
<option value="_self" selected="">_self</option>
|
||||
<option value="_blank">_blank</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-0">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarShowLink" checked="">
|
||||
<label class="form-check-label small" for="topBarShowLink" style="color: #495057;">
|
||||
<strong>Mostrar enlace</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,136 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Menu Class
|
||||
*
|
||||
* Registra menú en WordPress admin y carga assets
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class APUS_Admin_Menu {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('admin_menu', array($this, 'add_menu_page'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registrar página de admin
|
||||
*/
|
||||
public function add_menu_page() {
|
||||
add_theme_page(
|
||||
'APUs Theme Settings', // Page title
|
||||
'Tema APUs', // Menu title
|
||||
'manage_options', // Capability
|
||||
'apus-theme-settings', // Menu slug
|
||||
array($this, 'render_admin_page'), // Callback
|
||||
59 // Position
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderizar página de admin
|
||||
*/
|
||||
public function render_admin_page() {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die(__('No tienes permisos para acceder a esta página.'));
|
||||
}
|
||||
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'pages/main.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Encolar assets (CSS/JS)
|
||||
*/
|
||||
public function enqueue_assets($hook) {
|
||||
// Solo cargar en nuestra página
|
||||
if ($hook !== 'appearance_page_apus-theme-settings') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bootstrap 5.3.2 CSS
|
||||
wp_enqueue_style(
|
||||
'bootstrap',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css',
|
||||
array(),
|
||||
'5.3.2'
|
||||
);
|
||||
|
||||
// Bootstrap Icons
|
||||
wp_enqueue_style(
|
||||
'bootstrap-icons',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css',
|
||||
array(),
|
||||
'1.11.1'
|
||||
);
|
||||
|
||||
// Admin Panel CSS (Core)
|
||||
wp_enqueue_style(
|
||||
'apus-admin-panel-css',
|
||||
APUS_ADMIN_PANEL_URL . 'assets/css/admin-panel.css',
|
||||
array('bootstrap'),
|
||||
APUS_ADMIN_PANEL_VERSION
|
||||
);
|
||||
|
||||
// Component: Navbar CSS (estilos admin específicos)
|
||||
wp_enqueue_style(
|
||||
'apus-component-navbar-css',
|
||||
APUS_ADMIN_PANEL_URL . 'assets/css/component-navbar.css',
|
||||
array('apus-admin-panel-css'),
|
||||
APUS_ADMIN_PANEL_VERSION
|
||||
);
|
||||
|
||||
// Bootstrap 5.3.2 JS
|
||||
wp_enqueue_script(
|
||||
'bootstrap',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
|
||||
array(),
|
||||
'5.3.2',
|
||||
true
|
||||
);
|
||||
|
||||
// Axios (para AJAX)
|
||||
wp_enqueue_script(
|
||||
'axios',
|
||||
'https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js',
|
||||
array(),
|
||||
'1.6.0',
|
||||
true
|
||||
);
|
||||
|
||||
// Component: Navbar JS (cargar antes de admin-app.js)
|
||||
wp_enqueue_script(
|
||||
'apus-component-navbar-js',
|
||||
APUS_ADMIN_PANEL_URL . 'assets/js/component-navbar.js',
|
||||
array('jquery'),
|
||||
APUS_ADMIN_PANEL_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Admin Panel JS (Core - depende de componentes)
|
||||
wp_enqueue_script(
|
||||
'apus-admin-panel-js',
|
||||
APUS_ADMIN_PANEL_URL . 'assets/js/admin-app.js',
|
||||
array('jquery', 'axios', 'apus-component-navbar-js'),
|
||||
APUS_ADMIN_PANEL_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Pasar datos a JavaScript
|
||||
wp_localize_script('apus-admin-panel-js', 'apusAdminData', array(
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('apus_admin_nonce')
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Instanciar clase
|
||||
new APUS_Admin_Menu();
|
||||
@@ -1,310 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Data Migrator Class
|
||||
*
|
||||
* Migración de datos de wp_options a tabla personalizada
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.2.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class APUS_Data_Migrator {
|
||||
|
||||
/**
|
||||
* Opción para trackear si la migración se completó
|
||||
*/
|
||||
const MIGRATION_FLAG = 'apus_data_migrated';
|
||||
|
||||
/**
|
||||
* Opción antigua en wp_options
|
||||
*/
|
||||
const OLD_OPTION_NAME = 'apus_theme_settings';
|
||||
|
||||
/**
|
||||
* DB Manager instance
|
||||
*/
|
||||
private $db_manager;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->db_manager = new APUS_DB_Manager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si la migración ya se ejecutó
|
||||
*/
|
||||
public function is_migrated() {
|
||||
return get_option(self::MIGRATION_FLAG) === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecutar migración si es necesaria
|
||||
*/
|
||||
public function maybe_migrate() {
|
||||
if ($this->is_migrated()) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'La migración ya fue ejecutada anteriormente'
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->db_manager->table_exists()) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'La tabla de destino no existe'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->migrate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecutar migración completa
|
||||
*/
|
||||
public function migrate() {
|
||||
global $wpdb;
|
||||
|
||||
// Comenzar transacción
|
||||
$wpdb->query('START TRANSACTION');
|
||||
|
||||
try {
|
||||
// Obtener datos de wp_options
|
||||
$old_data = get_option(self::OLD_OPTION_NAME);
|
||||
|
||||
if (empty($old_data)) {
|
||||
throw new Exception('No hay datos para migrar en wp_options');
|
||||
}
|
||||
|
||||
$total_migrated = 0;
|
||||
|
||||
// Verificar estructura de datos
|
||||
if (!isset($old_data['components']) || !is_array($old_data['components'])) {
|
||||
throw new Exception('Estructura de datos inválida');
|
||||
}
|
||||
|
||||
// Obtener versión y timestamp
|
||||
$version = isset($old_data['version']) ? $old_data['version'] : APUS_ADMIN_PANEL_VERSION;
|
||||
|
||||
// Migrar cada componente
|
||||
foreach ($old_data['components'] as $component_name => $component_data) {
|
||||
if (!is_array($component_data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrate_component($component_name, $component_data, $version);
|
||||
$total_migrated += $migrated;
|
||||
}
|
||||
|
||||
// Marcar migración como completada
|
||||
update_option(self::MIGRATION_FLAG, '1', false);
|
||||
|
||||
// Commit transacción
|
||||
$wpdb->query('COMMIT');
|
||||
|
||||
error_log("APUS Data Migrator: Migración completada. Total de registros: $total_migrated");
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'Migración completada exitosamente',
|
||||
'total_migrated' => $total_migrated
|
||||
);
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Rollback en caso de error
|
||||
$wpdb->query('ROLLBACK');
|
||||
|
||||
error_log("APUS Data Migrator: Error en migración - " . $e->getMessage());
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'Error en migración: ' . $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrar un componente específico
|
||||
*
|
||||
* @param string $component_name Nombre del componente
|
||||
* @param array $component_data Datos del componente
|
||||
* @param string $version Versión
|
||||
* @return int Número de registros migrados
|
||||
*/
|
||||
private function migrate_component($component_name, $component_data, $version) {
|
||||
$count = 0;
|
||||
|
||||
foreach ($component_data as $key => $value) {
|
||||
// Determinar tipo de dato
|
||||
$data_type = $this->determine_data_type($key, $value);
|
||||
|
||||
// Si es un array/objeto anidado (como custom_styles), guardarlo como JSON
|
||||
if ($data_type === 'json') {
|
||||
$result = $this->db_manager->save_config(
|
||||
$component_name,
|
||||
$key,
|
||||
$value,
|
||||
$data_type,
|
||||
$version
|
||||
);
|
||||
} else {
|
||||
$result = $this->db_manager->save_config(
|
||||
$component_name,
|
||||
$key,
|
||||
$value,
|
||||
$data_type,
|
||||
$version
|
||||
);
|
||||
}
|
||||
|
||||
if ($result !== false) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determinar el tipo de dato
|
||||
*
|
||||
* @param string $key Clave de configuración
|
||||
* @param mixed $value Valor
|
||||
* @return string Tipo de dato (string, boolean, integer, json)
|
||||
*/
|
||||
private function determine_data_type($key, $value) {
|
||||
if (is_bool($value)) {
|
||||
return 'boolean';
|
||||
}
|
||||
|
||||
if (is_int($value)) {
|
||||
return 'integer';
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return 'json';
|
||||
}
|
||||
|
||||
// Por nombre de clave
|
||||
if (in_array($key, array('enabled', 'show_on_mobile', 'show_on_desktop', 'show_icon', 'show_link'))) {
|
||||
return 'boolean';
|
||||
}
|
||||
|
||||
return 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear backup de datos antiguos
|
||||
*
|
||||
* @return bool Éxito de la operación
|
||||
*/
|
||||
public function backup_old_data() {
|
||||
$old_data = get_option(self::OLD_OPTION_NAME);
|
||||
|
||||
if (empty($old_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$backup_option = self::OLD_OPTION_NAME . '_backup_' . time();
|
||||
return update_option($backup_option, $old_data, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaurar desde backup (rollback)
|
||||
*
|
||||
* @param string $backup_option Nombre de la opción de backup
|
||||
* @return bool Éxito de la operación
|
||||
*/
|
||||
public function rollback($backup_option) {
|
||||
$backup_data = get_option($backup_option);
|
||||
|
||||
if (empty($backup_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restaurar datos antiguos
|
||||
update_option(self::OLD_OPTION_NAME, $backup_data, false);
|
||||
|
||||
// Limpiar flag de migración
|
||||
delete_option(self::MIGRATION_FLAG);
|
||||
|
||||
// Limpiar tabla personalizada
|
||||
global $wpdb;
|
||||
$table_name = $this->db_manager->get_table_name();
|
||||
$wpdb->query("TRUNCATE TABLE $table_name");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparar datos entre wp_options y tabla personalizada
|
||||
*
|
||||
* @return array Resultado de la comparación
|
||||
*/
|
||||
public function verify_migration() {
|
||||
$old_data = get_option(self::OLD_OPTION_NAME);
|
||||
|
||||
if (empty($old_data) || !isset($old_data['components'])) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'No hay datos en wp_options para comparar'
|
||||
);
|
||||
}
|
||||
|
||||
$discrepancies = array();
|
||||
|
||||
foreach ($old_data['components'] as $component_name => $component_data) {
|
||||
$new_data = $this->db_manager->get_config($component_name);
|
||||
|
||||
foreach ($component_data as $key => $old_value) {
|
||||
$new_value = isset($new_data[$key]) ? $new_data[$key] : null;
|
||||
|
||||
// Comparar valores (teniendo en cuenta conversiones de tipo)
|
||||
if ($this->normalize_value($old_value) !== $this->normalize_value($new_value)) {
|
||||
$discrepancies[] = array(
|
||||
'component' => $component_name,
|
||||
'key' => $key,
|
||||
'old_value' => $old_value,
|
||||
'new_value' => $new_value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($discrepancies)) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'Migración verificada: todos los datos coinciden'
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'Se encontraron discrepancias en la migración',
|
||||
'discrepancies' => $discrepancies
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizar valor para comparación
|
||||
*
|
||||
* @param mixed $value Valor a normalizar
|
||||
* @return mixed Valor normalizado
|
||||
*/
|
||||
private function normalize_value($value) {
|
||||
if (is_bool($value)) {
|
||||
return $value ? 1 : 0;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return json_encode($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Database Manager Class
|
||||
*
|
||||
* Gestión de tablas personalizadas del tema
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.2.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class APUS_DB_Manager {
|
||||
|
||||
/**
|
||||
* Nombre de la tabla de componentes (sin prefijo)
|
||||
*/
|
||||
const TABLE_COMPONENTS = 'apus_theme_components';
|
||||
|
||||
/**
|
||||
* Versión de la base de datos
|
||||
*/
|
||||
const DB_VERSION = '1.0';
|
||||
|
||||
/**
|
||||
* Opción para almacenar la versión de la DB
|
||||
*/
|
||||
const DB_VERSION_OPTION = 'apus_db_version';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
// Hook para verificar/actualizar DB en cada carga
|
||||
add_action('admin_init', array($this, 'maybe_create_tables'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener nombre completo de tabla con prefijo
|
||||
*/
|
||||
public function get_table_name() {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . self::TABLE_COMPONENTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si las tablas necesitan ser creadas o actualizadas
|
||||
*/
|
||||
public function maybe_create_tables() {
|
||||
$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
|
||||
*/
|
||||
public function create_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$table_name = $this->get_table_name();
|
||||
|
||||
$sql = "CREATE TABLE $table_name (
|
||||
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;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($sql);
|
||||
|
||||
// Verificar si la tabla se creó correctamente
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name) {
|
||||
error_log("APUS DB Manager: Tabla $table_name creada/actualizada exitosamente");
|
||||
return true;
|
||||
} else {
|
||||
error_log("APUS DB Manager: Error al crear tabla $table_name");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si una tabla existe
|
||||
*/
|
||||
public function table_exists() {
|
||||
global $wpdb;
|
||||
$table_name = $this->get_table_name();
|
||||
return $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar configuración de un componente
|
||||
*
|
||||
* @param string $component_name Nombre del componente
|
||||
* @param string $config_key Clave de configuración
|
||||
* @param mixed $config_value Valor de configuración
|
||||
* @param string $data_type Tipo de dato (string, boolean, integer, json)
|
||||
* @param string $version Versión del tema
|
||||
* @return bool|int ID del registro o false en caso de error
|
||||
*/
|
||||
public function save_config($component_name, $config_key, $config_value, $data_type = 'string', $version = null) {
|
||||
global $wpdb;
|
||||
$table_name = $this->get_table_name();
|
||||
|
||||
// Convertir valor según tipo
|
||||
if ($data_type === 'json' && is_array($config_value)) {
|
||||
$config_value = json_encode($config_value, JSON_UNESCAPED_UNICODE);
|
||||
} elseif ($data_type === 'boolean') {
|
||||
$config_value = $config_value ? '1' : '0';
|
||||
}
|
||||
|
||||
// Usar ON DUPLICATE KEY UPDATE para INSERT o UPDATE
|
||||
$result = $wpdb->query($wpdb->prepare(
|
||||
"INSERT INTO $table_name (component_name, config_key, config_value, data_type, version, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
config_value = VALUES(config_value),
|
||||
data_type = VALUES(data_type),
|
||||
version = VALUES(version),
|
||||
updated_at = VALUES(updated_at)",
|
||||
$component_name,
|
||||
$config_key,
|
||||
$config_value,
|
||||
$data_type,
|
||||
$version,
|
||||
current_time('mysql')
|
||||
));
|
||||
|
||||
return $result !== false ? $wpdb->insert_id : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener configuración de un componente
|
||||
*
|
||||
* @param string $component_name Nombre del componente
|
||||
* @param string $config_key Clave específica (opcional)
|
||||
* @return array|mixed Configuración completa o valor específico
|
||||
*/
|
||||
public function get_config($component_name, $config_key = null) {
|
||||
global $wpdb;
|
||||
$table_name = $this->get_table_name();
|
||||
|
||||
if ($config_key !== null) {
|
||||
// Obtener un valor específico
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT config_value, data_type FROM $table_name
|
||||
WHERE component_name = %s AND config_key = %s",
|
||||
$component_name,
|
||||
$config_key
|
||||
));
|
||||
|
||||
if ($row) {
|
||||
return $this->parse_value($row->config_value, $row->data_type);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Obtener toda la configuración del componente
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT config_key, config_value, data_type FROM $table_name
|
||||
WHERE component_name = %s",
|
||||
$component_name
|
||||
));
|
||||
|
||||
$config = array();
|
||||
foreach ($rows as $row) {
|
||||
$config[$row->config_key] = $this->parse_value($row->config_value, $row->data_type);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsear valor según tipo de dato
|
||||
*
|
||||
* @param string $value Valor almacenado
|
||||
* @param string $data_type Tipo de dato
|
||||
* @return mixed Valor parseado
|
||||
*/
|
||||
private function parse_value($value, $data_type) {
|
||||
switch ($data_type) {
|
||||
case 'boolean':
|
||||
return (bool) $value;
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
case 'json':
|
||||
return json_decode($value, true);
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar configuraciones de un componente
|
||||
*
|
||||
* @param string $component_name Nombre del componente
|
||||
* @param string $config_key Clave específica (opcional)
|
||||
* @return bool Éxito de la operación
|
||||
*/
|
||||
public function delete_config($component_name, $config_key = null) {
|
||||
global $wpdb;
|
||||
$table_name = $this->get_table_name();
|
||||
|
||||
if ($config_key !== null) {
|
||||
return $wpdb->delete(
|
||||
$table_name,
|
||||
array(
|
||||
'component_name' => $component_name,
|
||||
'config_key' => $config_key
|
||||
),
|
||||
array('%s', '%s')
|
||||
) !== false;
|
||||
}
|
||||
|
||||
// Eliminar todas las configuraciones del componente
|
||||
return $wpdb->delete(
|
||||
$table_name,
|
||||
array('component_name' => $component_name),
|
||||
array('%s')
|
||||
) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar todos los componentes con configuraciones
|
||||
*
|
||||
* @return array Lista de nombres de componentes
|
||||
*/
|
||||
public function list_components() {
|
||||
global $wpdb;
|
||||
$table_name = $this->get_table_name();
|
||||
|
||||
return $wpdb->get_col(
|
||||
"SELECT DISTINCT component_name FROM $table_name ORDER BY component_name"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Settings Manager Class
|
||||
*
|
||||
* CRUD de configuraciones por componentes
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class APUS_Settings_Manager {
|
||||
|
||||
const OPTION_NAME = 'apus_theme_settings';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('wp_ajax_apus_get_settings', array($this, 'ajax_get_settings'));
|
||||
add_action('wp_ajax_apus_save_settings', array($this, 'ajax_save_settings'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener configuraciones
|
||||
*/
|
||||
public function get_settings() {
|
||||
$settings = get_option(self::OPTION_NAME, array());
|
||||
$defaults = $this->get_defaults();
|
||||
|
||||
return wp_parse_args($settings, $defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar configuraciones
|
||||
*/
|
||||
public function save_settings($data) {
|
||||
// Validar
|
||||
$validator = new APUS_Validator();
|
||||
$validation = $validator->validate($data);
|
||||
|
||||
if (!$validation['valid']) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'Error de validación',
|
||||
'errors' => $validation['errors']
|
||||
);
|
||||
}
|
||||
|
||||
// Sanitizar
|
||||
$sanitized = $this->sanitize_settings($data);
|
||||
|
||||
// Agregar metadata
|
||||
$sanitized['version'] = APUS_ADMIN_PANEL_VERSION;
|
||||
$sanitized['updated_at'] = current_time('mysql');
|
||||
|
||||
// Guardar
|
||||
update_option(self::OPTION_NAME, $sanitized, false);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'Configuración guardada correctamente'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valores por defecto
|
||||
* NOTA: Aquí se agregan los defaults de cada componente
|
||||
*/
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
'version' => APUS_ADMIN_PANEL_VERSION,
|
||||
'components' => array(
|
||||
'top_bar' => array(
|
||||
'enabled' => true,
|
||||
'show_on_mobile' => true,
|
||||
'show_on_desktop' => true,
|
||||
'icon_class' => 'bi bi-megaphone-fill',
|
||||
'show_icon' => true,
|
||||
'highlight_text' => 'Nuevo:',
|
||||
'message_text' => 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
|
||||
'link_text' => 'Ver Catálogo',
|
||||
'link_url' => '/catalogo',
|
||||
'link_target' => '_self',
|
||||
'show_link' => true,
|
||||
'custom_styles' => array(
|
||||
// Valores extraídos de componente-top-bar.css
|
||||
'background_color' => '#0E2337', // var(--color-navy-dark)
|
||||
'text_color' => '#ffffff',
|
||||
'highlight_color' => '#FF8600', // var(--color-orange-primary)
|
||||
'link_hover_color' => '#FF8600', // var(--color-orange-primary)
|
||||
'font_size' => 'normal' // 0.9rem del CSS
|
||||
)
|
||||
)
|
||||
// Navbar - Pendiente
|
||||
// Hero - Pendiente
|
||||
// Footer - Pendiente
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizar configuraciones
|
||||
* NOTA: Aquí se agrega sanitización de cada componente
|
||||
*/
|
||||
public function sanitize_settings($data) {
|
||||
$sanitized = array(
|
||||
'components' => array()
|
||||
);
|
||||
|
||||
// Sanitizar Top Bar
|
||||
if (isset($data['components']['top_bar'])) {
|
||||
$top_bar = $data['components']['top_bar'];
|
||||
|
||||
$sanitized['components']['top_bar'] = array(
|
||||
'enabled' => !empty($top_bar['enabled']),
|
||||
'show_on_mobile' => !empty($top_bar['show_on_mobile']),
|
||||
'show_on_desktop' => !empty($top_bar['show_on_desktop']),
|
||||
'icon_class' => sanitize_text_field($top_bar['icon_class'] ?? ''),
|
||||
'show_icon' => !empty($top_bar['show_icon']),
|
||||
'highlight_text' => sanitize_text_field($top_bar['highlight_text'] ?? ''),
|
||||
'message_text' => sanitize_text_field($top_bar['message_text'] ?? ''),
|
||||
'link_text' => sanitize_text_field($top_bar['link_text'] ?? ''),
|
||||
'link_url' => esc_url_raw($top_bar['link_url'] ?? ''),
|
||||
'link_target' => in_array($top_bar['link_target'] ?? '', array('_self', '_blank')) ? $top_bar['link_target'] : '_self',
|
||||
'show_link' => !empty($top_bar['show_link']),
|
||||
'custom_styles' => array(
|
||||
'background_color' => sanitize_hex_color($top_bar['custom_styles']['background_color'] ?? ''),
|
||||
'text_color' => sanitize_hex_color($top_bar['custom_styles']['text_color'] ?? ''),
|
||||
'highlight_color' => sanitize_hex_color($top_bar['custom_styles']['highlight_color'] ?? ''),
|
||||
'link_hover_color' => sanitize_hex_color($top_bar['custom_styles']['link_hover_color'] ?? ''),
|
||||
'font_size' => in_array($top_bar['custom_styles']['font_size'] ?? '', array('small', 'normal', 'large')) ? $top_bar['custom_styles']['font_size'] : 'normal'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Obtener configuraciones
|
||||
*/
|
||||
public function ajax_get_settings() {
|
||||
// Verificar nonce usando check_ajax_referer (método recomendado para AJAX)
|
||||
check_ajax_referer('apus_admin_nonce', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error('Permisos insuficientes');
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
wp_send_json_success($settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Guardar configuraciones
|
||||
*/
|
||||
public function ajax_save_settings() {
|
||||
// Verificar nonce usando check_ajax_referer (método recomendado para AJAX)
|
||||
check_ajax_referer('apus_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 APUS_Settings_Manager();
|
||||
@@ -1,382 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Options Migrator Class
|
||||
*
|
||||
* Migra configuraciones de wp_options a tabla personalizada wp_apus_theme_components
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class APUS_Theme_Options_Migrator {
|
||||
|
||||
/**
|
||||
* DB Manager instance
|
||||
*/
|
||||
private $db_manager;
|
||||
|
||||
/**
|
||||
* Nombre de la opción en wp_options
|
||||
*/
|
||||
const OLD_OPTION_NAME = 'apus_theme_options';
|
||||
|
||||
/**
|
||||
* Nombre del componente en la nueva tabla
|
||||
*/
|
||||
const COMPONENT_NAME = 'theme';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->db_manager = new APUS_DB_Manager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapeo de tipos de datos para cada configuración
|
||||
*
|
||||
* @return array Mapeo config_key => data_type
|
||||
*/
|
||||
private function get_data_types_map() {
|
||||
return array(
|
||||
// Integers (IDs y contadores)
|
||||
'site_logo' => 'integer',
|
||||
'site_favicon' => 'integer',
|
||||
'excerpt_length' => 'integer',
|
||||
'archive_posts_per_page' => 'integer',
|
||||
'related_posts_count' => 'integer',
|
||||
'related_posts_columns' => 'integer',
|
||||
|
||||
// Booleans (enable_*, show_*, performance_*)
|
||||
'enable_breadcrumbs' => 'boolean',
|
||||
'show_featured_image_single' => 'boolean',
|
||||
'show_author_box' => 'boolean',
|
||||
'enable_comments_posts' => 'boolean',
|
||||
'enable_comments_pages' => 'boolean',
|
||||
'show_post_meta' => 'boolean',
|
||||
'show_post_tags' => 'boolean',
|
||||
'show_post_categories' => 'boolean',
|
||||
'enable_lazy_loading' => 'boolean',
|
||||
'performance_remove_emoji' => 'boolean',
|
||||
'performance_remove_embeds' => 'boolean',
|
||||
'performance_remove_dashicons' => 'boolean',
|
||||
'performance_defer_js' => 'boolean',
|
||||
'performance_minify_html' => 'boolean',
|
||||
'performance_disable_gutenberg' => 'boolean',
|
||||
'enable_related_posts' => 'boolean',
|
||||
|
||||
// Strings (todo lo demás: URLs, textos cortos, formatos, CSS/JS)
|
||||
// No es necesario especificarlos, 'string' es el default
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determinar tipo de dato para una configuración
|
||||
*
|
||||
* @param string $config_key Nombre de la configuración
|
||||
* @param mixed $config_value Valor de la configuración
|
||||
* @return string Tipo de dato (string, boolean, integer, json)
|
||||
*/
|
||||
private function determine_data_type($config_key, $config_value) {
|
||||
$types_map = $this->get_data_types_map();
|
||||
|
||||
// Si está en el mapa explícito, usar ese tipo
|
||||
if (isset($types_map[$config_key])) {
|
||||
return $types_map[$config_key];
|
||||
}
|
||||
|
||||
// Detección automática por valor
|
||||
if (is_array($config_value)) {
|
||||
return 'json';
|
||||
}
|
||||
|
||||
if (is_bool($config_value)) {
|
||||
return 'boolean';
|
||||
}
|
||||
|
||||
if (is_int($config_value)) {
|
||||
return 'integer';
|
||||
}
|
||||
|
||||
// Default: string (incluye textos largos, URLs, etc.)
|
||||
return 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizar valor según tipo de dato
|
||||
*
|
||||
* @param mixed $value Valor a normalizar
|
||||
* @param string $data_type Tipo de dato
|
||||
* @return mixed Valor normalizado
|
||||
*/
|
||||
private function normalize_value($value, $data_type) {
|
||||
switch ($data_type) {
|
||||
case 'boolean':
|
||||
// Convertir a booleano real (maneja strings '0', '1', etc.)
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
|
||||
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
|
||||
case 'json':
|
||||
// Si ya es array, dejarlo así (DB Manager lo codificará)
|
||||
return is_array($value) ? $value : json_decode($value, true);
|
||||
|
||||
case 'string':
|
||||
default:
|
||||
return (string) $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si ya se realizó la migración
|
||||
*
|
||||
* @return bool True si ya está migrado, false si no
|
||||
*/
|
||||
public function is_migrated() {
|
||||
// La migración se considera completa si:
|
||||
// 1. No existe la opción antigua en wp_options
|
||||
// 2. Y existen configuraciones en la tabla nueva
|
||||
|
||||
$old_options = get_option(self::OLD_OPTION_NAME, false);
|
||||
$new_config = $this->db_manager->get_config(self::COMPONENT_NAME);
|
||||
|
||||
// Si no hay opción antigua Y hay configuraciones nuevas = migrado
|
||||
return ($old_options === false && !empty($new_config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecutar migración completa
|
||||
*
|
||||
* @return array Resultado de la migración con éxito, mensaje y detalles
|
||||
*/
|
||||
public function migrate() {
|
||||
// 1. Verificar si ya se migró
|
||||
if ($this->is_migrated()) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'La migración ya fue realizada anteriormente',
|
||||
'already_migrated' => true
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Obtener configuraciones actuales de wp_options
|
||||
$old_options = get_option(self::OLD_OPTION_NAME, array());
|
||||
|
||||
if (empty($old_options)) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'No hay opciones para migrar en wp_options'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Crear backup antes de migrar
|
||||
$backup_result = $this->create_backup($old_options);
|
||||
if (!$backup_result['success']) {
|
||||
return $backup_result;
|
||||
}
|
||||
|
||||
$backup_name = $backup_result['backup_name'];
|
||||
|
||||
// 4. Migrar cada configuración
|
||||
$total = count($old_options);
|
||||
$migrated = 0;
|
||||
$errors = array();
|
||||
|
||||
foreach ($old_options as $config_key => $config_value) {
|
||||
// Determinar tipo de dato
|
||||
$data_type = $this->determine_data_type($config_key, $config_value);
|
||||
|
||||
// Normalizar valor
|
||||
$normalized_value = $this->normalize_value($config_value, $data_type);
|
||||
|
||||
// Guardar en tabla personalizada
|
||||
$result = $this->db_manager->save_config(
|
||||
self::COMPONENT_NAME,
|
||||
$config_key,
|
||||
$normalized_value,
|
||||
$data_type,
|
||||
APUS_ADMIN_PANEL_VERSION
|
||||
);
|
||||
|
||||
if ($result !== false) {
|
||||
$migrated++;
|
||||
} else {
|
||||
$errors[] = $config_key;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Verificar resultado de la migración
|
||||
if ($migrated === $total) {
|
||||
// Éxito total
|
||||
// Eliminar opción antigua de wp_options
|
||||
delete_option(self::OLD_OPTION_NAME);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => sprintf('Migradas %d configuraciones correctamente', $migrated),
|
||||
'migrated' => $migrated,
|
||||
'total' => $total,
|
||||
'backup_name' => $backup_name
|
||||
);
|
||||
} else {
|
||||
// Migración parcial o con errores
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => sprintf('Solo se migraron %d de %d configuraciones', $migrated, $total),
|
||||
'migrated' => $migrated,
|
||||
'total' => $total,
|
||||
'errors' => $errors,
|
||||
'backup_name' => $backup_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear backup de las opciones actuales
|
||||
*
|
||||
* @param array $options Opciones a respaldar
|
||||
* @return array Resultado con success y backup_name
|
||||
*/
|
||||
private function create_backup($options) {
|
||||
$backup_name = self::OLD_OPTION_NAME . '_backup_' . date('Y-m-d_H-i-s');
|
||||
|
||||
$result = update_option($backup_name, $options, false); // No autoload
|
||||
|
||||
if ($result) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'backup_name' => $backup_name
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'No se pudo crear el backup de seguridad'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback de migración (revertir a estado anterior)
|
||||
*
|
||||
* @param string $backup_name Nombre del backup a restaurar
|
||||
* @return array Resultado del rollback
|
||||
*/
|
||||
public function rollback($backup_name = null) {
|
||||
// Si no se especifica backup, buscar el más reciente
|
||||
if ($backup_name === null) {
|
||||
$backup_name = $this->find_latest_backup();
|
||||
}
|
||||
|
||||
if ($backup_name === null) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'No se encontró backup para restaurar'
|
||||
);
|
||||
}
|
||||
|
||||
// Obtener backup
|
||||
$backup = get_option($backup_name, false);
|
||||
|
||||
if ($backup === false) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => sprintf('Backup "%s" no encontrado', $backup_name)
|
||||
);
|
||||
}
|
||||
|
||||
// Restaurar en wp_options
|
||||
$restored = update_option(self::OLD_OPTION_NAME, $backup);
|
||||
|
||||
if ($restored) {
|
||||
// Eliminar configuraciones de la tabla personalizada
|
||||
$this->db_manager->delete_config(self::COMPONENT_NAME);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => 'Rollback completado exitosamente',
|
||||
'backup_used' => $backup_name
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => 'No se pudo restaurar el backup'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar el backup más reciente
|
||||
*
|
||||
* @return string|null Nombre del backup más reciente o null
|
||||
*/
|
||||
private function find_latest_backup() {
|
||||
global $wpdb;
|
||||
|
||||
// Buscar opciones que empiecen con el patrón de backup
|
||||
$pattern = self::OLD_OPTION_NAME . '_backup_%';
|
||||
|
||||
$backup_name = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT option_name FROM {$wpdb->options}
|
||||
WHERE option_name LIKE %s
|
||||
ORDER BY option_id DESC
|
||||
LIMIT 1",
|
||||
$pattern
|
||||
));
|
||||
|
||||
return $backup_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar todos los backups disponibles
|
||||
*
|
||||
* @return array Lista de nombres de backups
|
||||
*/
|
||||
public function list_backups() {
|
||||
global $wpdb;
|
||||
|
||||
$pattern = self::OLD_OPTION_NAME . '_backup_%';
|
||||
|
||||
$backups = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT option_name FROM {$wpdb->options}
|
||||
WHERE option_name LIKE %s
|
||||
ORDER BY option_id DESC",
|
||||
$pattern
|
||||
));
|
||||
|
||||
return $backups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar un backup específico
|
||||
*
|
||||
* @param string $backup_name Nombre del backup a eliminar
|
||||
* @return bool True si se eliminó, false si no
|
||||
*/
|
||||
public function delete_backup($backup_name) {
|
||||
return delete_option($backup_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener estadísticas de la migración
|
||||
*
|
||||
* @return array Estadísticas
|
||||
*/
|
||||
public function get_migration_stats() {
|
||||
$old_options = get_option(self::OLD_OPTION_NAME, array());
|
||||
$new_config = $this->db_manager->get_config(self::COMPONENT_NAME);
|
||||
$backups = $this->list_backups();
|
||||
|
||||
return array(
|
||||
'is_migrated' => $this->is_migrated(),
|
||||
'old_options_count' => count($old_options),
|
||||
'new_config_count' => count($new_config),
|
||||
'backups_count' => count($backups),
|
||||
'backups' => $backups
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Validator Class
|
||||
*
|
||||
* Validación de datos por componentes
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class APUS_Validator {
|
||||
|
||||
/**
|
||||
* Validar todas las configuraciones
|
||||
*/
|
||||
public function validate($data) {
|
||||
$errors = array();
|
||||
|
||||
// Validar estructura base
|
||||
if (!isset($data['components']) || !is_array($data['components'])) {
|
||||
$errors[] = 'Estructura de datos inválida';
|
||||
return array('valid' => false, 'errors' => $errors);
|
||||
}
|
||||
|
||||
// Validar Top Bar
|
||||
if (isset($data['components']['top_bar'])) {
|
||||
$top_bar_errors = $this->validate_top_bar($data['components']['top_bar']);
|
||||
$errors = array_merge($errors, $top_bar_errors);
|
||||
}
|
||||
|
||||
return array(
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar Top Bar
|
||||
*/
|
||||
public function validate_top_bar($top_bar) {
|
||||
$errors = array();
|
||||
|
||||
// Validar icon_class
|
||||
if (!empty($top_bar['icon_class']) && strlen($top_bar['icon_class']) > 50) {
|
||||
$errors[] = 'La clase del icono no puede exceder 50 caracteres';
|
||||
}
|
||||
|
||||
// Validar highlight_text
|
||||
if (!empty($top_bar['highlight_text']) && strlen($top_bar['highlight_text']) > 30) {
|
||||
$errors[] = 'El texto destacado no puede exceder 30 caracteres';
|
||||
}
|
||||
|
||||
// Validar message_text
|
||||
if (empty($top_bar['message_text'])) {
|
||||
$errors[] = 'El mensaje principal es obligatorio';
|
||||
} elseif (strlen($top_bar['message_text']) > 250) {
|
||||
$errors[] = 'El mensaje principal no puede exceder 250 caracteres';
|
||||
}
|
||||
|
||||
// Validar link_text
|
||||
if (!empty($top_bar['link_text']) && strlen($top_bar['link_text']) > 50) {
|
||||
$errors[] = 'El texto del enlace no puede exceder 50 caracteres';
|
||||
}
|
||||
|
||||
// Validar link_url (acepta URLs completas y relativas que empiecen con /)
|
||||
if (!empty($top_bar['link_url'])) {
|
||||
$url = $top_bar['link_url'];
|
||||
$is_valid_url = filter_var($url, FILTER_VALIDATE_URL) !== false;
|
||||
$is_relative_url = preg_match('/^\//', $url);
|
||||
|
||||
if (!$is_valid_url && !$is_relative_url) {
|
||||
$errors[] = 'La URL del enlace no es válida';
|
||||
}
|
||||
}
|
||||
|
||||
// Validar link_target
|
||||
if (!in_array($top_bar['link_target'] ?? '', array('_self', '_blank'))) {
|
||||
$errors[] = 'El target del enlace debe ser _self o _blank';
|
||||
}
|
||||
|
||||
// Validar colores
|
||||
if (!empty($top_bar['custom_styles']['background_color']) && !preg_match('/^#[a-f0-9]{6}$/i', $top_bar['custom_styles']['background_color'])) {
|
||||
$errors[] = 'El color de fondo debe ser un color hexadecimal válido';
|
||||
}
|
||||
if (!empty($top_bar['custom_styles']['text_color']) && !preg_match('/^#[a-f0-9]{6}$/i', $top_bar['custom_styles']['text_color'])) {
|
||||
$errors[] = 'El color de texto debe ser un color hexadecimal válido';
|
||||
}
|
||||
if (!empty($top_bar['custom_styles']['highlight_color']) && !preg_match('/^#[a-f0-9]{6}$/i', $top_bar['custom_styles']['highlight_color'])) {
|
||||
$errors[] = 'El color del highlight debe ser un color hexadecimal válido';
|
||||
}
|
||||
if (!empty($top_bar['custom_styles']['link_hover_color']) && !preg_match('/^#[a-f0-9]{6}$/i', $top_bar['custom_styles']['link_hover_color'])) {
|
||||
$errors[] = 'El color hover del enlace debe ser un color hexadecimal válido';
|
||||
}
|
||||
|
||||
// Validar font_size
|
||||
if (!in_array($top_bar['custom_styles']['font_size'] ?? '', array('small', 'normal', 'large'))) {
|
||||
$errors[] = 'El tamaño de fuente debe ser small, normal o large';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Hero Section Sanitizer
|
||||
*
|
||||
* Sanitiza configuraciones del componente Hero Section
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @subpackage Admin_Panel\Sanitizers
|
||||
* @since 2.1.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class APUS_HeroSection_Sanitizer
|
||||
*
|
||||
* Sanitiza todas las configuraciones del componente Hero Section
|
||||
*/
|
||||
class APUS_HeroSection_Sanitizer {
|
||||
|
||||
/**
|
||||
* Obtiene los valores por defecto del Hero Section
|
||||
*
|
||||
* @return array Valores por defecto
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
// Activación y Visibilidad
|
||||
'enabled' => true,
|
||||
'show_on_mobile' => true,
|
||||
'show_on_desktop' => true,
|
||||
|
||||
// Contenido y Estructura
|
||||
'show_category_badges' => true,
|
||||
'category_badge_icon' => 'bi bi-folder-fill',
|
||||
'excluded_categories' => array('Uncategorized', 'Sin categoría'),
|
||||
'title_alignment' => 'center',
|
||||
'title_display_class' => 'display-5',
|
||||
|
||||
// Colores del Hero
|
||||
'use_gradient_background' => true,
|
||||
'gradient_start_color' => '#1e3a5f',
|
||||
'gradient_end_color' => '#2c5282',
|
||||
'gradient_angle' => 135,
|
||||
'hero_text_color' => '#ffffff',
|
||||
'solid_background_color' => '#1e3a5f',
|
||||
|
||||
// Colores de Category Badges
|
||||
'badge_bg_color' => 'rgba(255, 255, 255, 0.15)',
|
||||
'badge_bg_hover_color' => 'rgba(255, 133, 0, 0.2)',
|
||||
'badge_border_color' => 'rgba(255, 255, 255, 0.2)',
|
||||
'badge_text_color' => 'rgba(255, 255, 255, 0.95)',
|
||||
'badge_icon_color' => '#FFB800',
|
||||
|
||||
// Espaciado y Dimensiones
|
||||
'hero_padding_vertical' => 3.0,
|
||||
'hero_padding_horizontal' => 0.0,
|
||||
'hero_margin_bottom' => 1.5,
|
||||
'badges_gap' => 0.5,
|
||||
'badge_padding_vertical' => 0.375,
|
||||
'badge_padding_horizontal' => 0.875,
|
||||
'badge_border_radius' => 20,
|
||||
|
||||
// Tipografía
|
||||
'h1_font_weight' => 700,
|
||||
'badge_font_size' => 0.813,
|
||||
'badge_font_weight' => 500,
|
||||
'h1_line_height' => 1.4,
|
||||
|
||||
// Efectos Visuales
|
||||
'enable_h1_text_shadow' => true,
|
||||
'h1_text_shadow' => '1px 1px 2px rgba(0, 0, 0, 0.2)',
|
||||
'enable_hero_box_shadow' => true,
|
||||
'hero_box_shadow' => '0 4px 16px rgba(30, 58, 95, 0.25)',
|
||||
'enable_badge_backdrop_filter' => true,
|
||||
'badge_backdrop_filter' => 'blur(10px)',
|
||||
|
||||
// Transiciones y Animaciones
|
||||
'badge_transition_speed' => 'normal',
|
||||
'badge_hover_effect' => 'background',
|
||||
|
||||
// Avanzado
|
||||
'custom_hero_classes' => '',
|
||||
'custom_badge_classes' => ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza los datos del Hero Section
|
||||
*
|
||||
* @param array $data Datos sin sanitizar del Hero Section
|
||||
* @return array Datos sanitizados
|
||||
*/
|
||||
public function sanitize($data) {
|
||||
return array_merge(
|
||||
// Activación y Visibilidad - Booleanos
|
||||
APUS_Sanitizer_Helper::sanitize_booleans($data, array(
|
||||
'enabled', 'show_on_mobile', 'show_on_desktop', 'show_category_badges',
|
||||
'use_gradient_background', 'enable_h1_text_shadow', 'enable_hero_box_shadow',
|
||||
'enable_badge_backdrop_filter'
|
||||
)),
|
||||
|
||||
// Contenido y Estructura - Textos
|
||||
APUS_Sanitizer_Helper::sanitize_texts($data, array(
|
||||
'category_badge_icon' => 'bi bi-folder-fill',
|
||||
'title_display_class' => 'display-5',
|
||||
'h1_text_shadow' => '1px 1px 2px rgba(0, 0, 0, 0.2)',
|
||||
'hero_box_shadow' => '0 4px 16px rgba(30, 58, 95, 0.25)',
|
||||
'badge_backdrop_filter' => 'blur(10px)',
|
||||
'custom_hero_classes' => '',
|
||||
'custom_badge_classes' => ''
|
||||
)),
|
||||
|
||||
// Colores de Category Badges - RGBA strings (text)
|
||||
array(
|
||||
'badge_bg_color' => APUS_Sanitizer_Helper::sanitize_text($data, 'badge_bg_color', 'rgba(255, 255, 255, 0.15)'),
|
||||
'badge_bg_hover_color' => APUS_Sanitizer_Helper::sanitize_text($data, 'badge_bg_hover_color', 'rgba(255, 133, 0, 0.2)'),
|
||||
'badge_border_color' => APUS_Sanitizer_Helper::sanitize_text($data, 'badge_border_color', 'rgba(255, 255, 255, 0.2)'),
|
||||
'badge_text_color' => APUS_Sanitizer_Helper::sanitize_text($data, 'badge_text_color', 'rgba(255, 255, 255, 0.95)')
|
||||
),
|
||||
|
||||
// Colores del Hero - Hex colors
|
||||
array(
|
||||
'gradient_start_color' => APUS_Sanitizer_Helper::sanitize_color($data, 'gradient_start_color', '#1e3a5f'),
|
||||
'gradient_end_color' => APUS_Sanitizer_Helper::sanitize_color($data, 'gradient_end_color', '#2c5282'),
|
||||
'hero_text_color' => APUS_Sanitizer_Helper::sanitize_color($data, 'hero_text_color', '#ffffff'),
|
||||
'solid_background_color' => APUS_Sanitizer_Helper::sanitize_color($data, 'solid_background_color', '#1e3a5f'),
|
||||
'badge_icon_color' => APUS_Sanitizer_Helper::sanitize_color($data, 'badge_icon_color', '#FFB800')
|
||||
),
|
||||
|
||||
// Enums
|
||||
APUS_Sanitizer_Helper::sanitize_enums($data, array(
|
||||
'title_alignment' => array('allowed' => array('left', 'center', 'right'), 'default' => 'center'),
|
||||
'badge_transition_speed' => array('allowed' => array('fast', 'normal', 'slow'), 'default' => 'normal'),
|
||||
'badge_hover_effect' => array('allowed' => array('none', 'background', 'scale', 'brightness'), 'default' => 'background')
|
||||
)),
|
||||
|
||||
// Enteros
|
||||
APUS_Sanitizer_Helper::sanitize_ints($data, array(
|
||||
'gradient_angle' => 135,
|
||||
'badge_border_radius' => 20
|
||||
)),
|
||||
|
||||
// Enteros en arrays (h1_font_weight, badge_font_weight)
|
||||
array(
|
||||
'h1_font_weight' => APUS_Sanitizer_Helper::sanitize_enum($data, 'h1_font_weight', array(400, 500, 600, 700), 700),
|
||||
'badge_font_weight' => APUS_Sanitizer_Helper::sanitize_enum($data, 'badge_font_weight', array(400, 500, 600, 700), 500)
|
||||
),
|
||||
|
||||
// Floats
|
||||
APUS_Sanitizer_Helper::sanitize_floats($data, array(
|
||||
'hero_padding_vertical' => 3.0,
|
||||
'hero_padding_horizontal' => 0.0,
|
||||
'hero_margin_bottom' => 1.5,
|
||||
'badges_gap' => 0.5,
|
||||
'badge_padding_vertical' => 0.375,
|
||||
'badge_padding_horizontal' => 0.875,
|
||||
'badge_font_size' => 0.813,
|
||||
'h1_line_height' => 1.4
|
||||
)),
|
||||
|
||||
// Array de strings
|
||||
array('excluded_categories' => APUS_Sanitizer_Helper::sanitize_array_of_strings(
|
||||
$data,
|
||||
'excluded_categories',
|
||||
array('Uncategorized', 'Sin categoría')
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Let's Talk Button Sanitizer
|
||||
*
|
||||
* Sanitiza configuraciones del componente Let's Talk Button
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @subpackage Admin_Panel\Sanitizers
|
||||
* @since 2.1.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class APUS_LetsTalkButton_Sanitizer
|
||||
*
|
||||
* Sanitiza todas las configuraciones del componente Let's Talk Button
|
||||
*/
|
||||
class APUS_LetsTalkButton_Sanitizer {
|
||||
|
||||
/**
|
||||
* Obtiene los valores por defecto del Let's Talk Button
|
||||
*
|
||||
* @return array Valores por defecto
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
'enabled' => true,
|
||||
'text' => "Let's Talk",
|
||||
'icon_class' => 'bi bi-lightning-charge-fill',
|
||||
'show_icon' => true,
|
||||
'position' => 'right',
|
||||
'enable_box_shadow' => false,
|
||||
'hover_effect' => 'none',
|
||||
'modal_target' => '#contactModal',
|
||||
'custom_styles' => array(
|
||||
'background_color' => '#FF8600',
|
||||
'background_hover_color' => '#FF6B35',
|
||||
'text_color' => '#ffffff',
|
||||
'icon_color' => '#ffffff',
|
||||
'font_weight' => '600',
|
||||
'padding_vertical' => 0.5,
|
||||
'padding_horizontal' => 1.5,
|
||||
'border_radius' => 6,
|
||||
'border_width' => 0,
|
||||
'border_color' => '',
|
||||
'border_style' => 'solid',
|
||||
'transition_speed' => 'normal',
|
||||
'box_shadow' => '0 2px 8px rgba(0, 0, 0, 0.15)'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza los datos del Let's Talk Button
|
||||
*
|
||||
* @param array $data Datos sin sanitizar del Let's Talk Button
|
||||
* @return array Datos sanitizados
|
||||
*/
|
||||
public function sanitize($data) {
|
||||
return array_merge(
|
||||
// Booleanos
|
||||
APUS_Sanitizer_Helper::sanitize_booleans($data, array(
|
||||
'enabled', 'show_icon', 'enable_box_shadow'
|
||||
)),
|
||||
|
||||
// Textos
|
||||
APUS_Sanitizer_Helper::sanitize_texts($data, array(
|
||||
'text', 'icon_class', 'modal_target'
|
||||
)),
|
||||
|
||||
// Enums
|
||||
APUS_Sanitizer_Helper::sanitize_enums($data, array(
|
||||
'position' => array('allowed' => array('left', 'center', 'right'), 'default' => 'right'),
|
||||
'hover_effect' => array('allowed' => array('none', 'scale', 'brightness'), 'default' => 'none')
|
||||
)),
|
||||
|
||||
// Custom styles anidado
|
||||
array('custom_styles' => APUS_Sanitizer_Helper::sanitize_nested_group($data, 'custom_styles', array(
|
||||
'background_color' => array('type' => 'color', 'default' => ''),
|
||||
'background_hover_color' => array('type' => 'color', 'default' => ''),
|
||||
'text_color' => array('type' => 'color', 'default' => ''),
|
||||
'icon_color' => array('type' => 'color', 'default' => ''),
|
||||
'font_weight' => array('type' => 'text', 'default' => ''),
|
||||
'padding_vertical' => array('type' => 'float', 'default' => 0.0),
|
||||
'padding_horizontal' => array('type' => 'float', 'default' => 0.0),
|
||||
'border_radius' => array('type' => 'int', 'default' => 0),
|
||||
'border_width' => array('type' => 'int', 'default' => 0),
|
||||
'border_color' => array('type' => 'color', 'default' => ''),
|
||||
'border_style' => array('type' => 'enum', 'allowed' => array('solid', 'dashed', 'dotted'), 'default' => 'solid'),
|
||||
'transition_speed' => array('type' => 'enum', 'allowed' => array('fast', 'normal', 'slow'), 'default' => 'normal'),
|
||||
'box_shadow' => array('type' => 'text', 'default' => '')
|
||||
)))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Navbar Sanitizer
|
||||
*
|
||||
* Sanitiza configuraciones del componente Navbar
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @subpackage Admin_Panel\Sanitizers
|
||||
* @since 2.1.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class APUS_Navbar_Sanitizer
|
||||
*
|
||||
* Sanitiza todas las configuraciones del componente Navbar
|
||||
*/
|
||||
class APUS_Navbar_Sanitizer {
|
||||
|
||||
/**
|
||||
* Obtiene los valores por defecto del Navbar
|
||||
*
|
||||
* @return array Valores por defecto
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
'enabled' => true,
|
||||
'show_on_mobile' => true,
|
||||
'show_on_desktop' => true,
|
||||
'position' => 'sticky',
|
||||
'responsive_breakpoint' => 'lg',
|
||||
'enable_box_shadow' => true,
|
||||
'enable_underline_effect' => true,
|
||||
'enable_hover_background' => true,
|
||||
|
||||
'lets_talk_button' => array(
|
||||
'enabled' => true,
|
||||
'text' => "Let's Talk",
|
||||
'icon_class' => 'bi bi-lightning-charge-fill',
|
||||
'show_icon' => true,
|
||||
'position' => 'right'
|
||||
),
|
||||
|
||||
'dropdown' => array(
|
||||
'enable_hover_desktop' => true,
|
||||
'max_height' => 70,
|
||||
'border_radius' => 8,
|
||||
'item_padding_vertical' => 0.5,
|
||||
'item_padding_horizontal' => 1.25
|
||||
),
|
||||
|
||||
'custom_styles' => array(
|
||||
'background_color' => '#1e3a5f',
|
||||
'text_color' => '#ffffff',
|
||||
'link_hover_color' => '#FF8600',
|
||||
'link_hover_bg_color' => '#FF8600',
|
||||
'dropdown_bg_color' => '#ffffff',
|
||||
'dropdown_item_color' => '#4A5568',
|
||||
'dropdown_item_hover_color' => '#FF8600',
|
||||
'font_size' => 'normal',
|
||||
'font_weight' => '500',
|
||||
'box_shadow_intensity' => 'normal',
|
||||
'border_radius' => 4,
|
||||
'padding_vertical' => 0.75,
|
||||
'link_padding_vertical' => 0.5,
|
||||
'link_padding_horizontal' => 0.65,
|
||||
'z_index' => 1030,
|
||||
'transition_speed' => 'normal'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza los datos del Navbar
|
||||
*
|
||||
* @param array $data Datos sin sanitizar del Navbar
|
||||
* @return array Datos sanitizados
|
||||
*/
|
||||
public function sanitize($data) {
|
||||
return array_merge(
|
||||
// Booleanos principales
|
||||
APUS_Sanitizer_Helper::sanitize_booleans($data, array(
|
||||
'enabled', 'show_on_mobile', 'show_on_desktop',
|
||||
'enable_box_shadow', 'enable_underline_effect', 'enable_hover_background'
|
||||
)),
|
||||
|
||||
// Enums principales
|
||||
APUS_Sanitizer_Helper::sanitize_enums($data, array(
|
||||
'position' => array('allowed' => array('sticky', 'static', 'fixed'), 'default' => 'sticky'),
|
||||
'responsive_breakpoint' => array('allowed' => array('sm', 'md', 'lg', 'xl', 'xxl'), 'default' => 'lg')
|
||||
)),
|
||||
|
||||
// Let's Talk Button anidado
|
||||
array('lets_talk_button' => APUS_Sanitizer_Helper::sanitize_nested_group($data, 'lets_talk_button', array(
|
||||
'enabled' => array('type' => 'bool'),
|
||||
'text' => array('type' => 'text', 'default' => ''),
|
||||
'icon_class' => array('type' => 'text', 'default' => ''),
|
||||
'show_icon' => array('type' => 'bool'),
|
||||
'position' => array('type' => 'enum', 'allowed' => array('left', 'center', 'right'), 'default' => 'right')
|
||||
))),
|
||||
|
||||
// Dropdown anidado
|
||||
array('dropdown' => APUS_Sanitizer_Helper::sanitize_nested_group($data, 'dropdown', array(
|
||||
'enable_hover_desktop' => array('type' => 'bool'),
|
||||
'max_height' => array('type' => 'int', 'default' => 70),
|
||||
'border_radius' => array('type' => 'int', 'default' => 8),
|
||||
'item_padding_vertical' => array('type' => 'float', 'default' => 0.5),
|
||||
'item_padding_horizontal' => array('type' => 'float', 'default' => 1.25)
|
||||
))),
|
||||
|
||||
// Custom styles anidado
|
||||
array('custom_styles' => APUS_Sanitizer_Helper::sanitize_nested_group($data, 'custom_styles', array(
|
||||
'background_color' => array('type' => 'color', 'default' => ''),
|
||||
'text_color' => array('type' => 'color', 'default' => ''),
|
||||
'link_hover_color' => array('type' => 'color', 'default' => ''),
|
||||
'link_hover_bg_color' => array('type' => 'color', 'default' => ''),
|
||||
'dropdown_bg_color' => array('type' => 'color', 'default' => ''),
|
||||
'dropdown_item_color' => array('type' => 'color', 'default' => ''),
|
||||
'dropdown_item_hover_color' => array('type' => 'color', 'default' => ''),
|
||||
'font_size' => array('type' => 'enum', 'allowed' => array('small', 'normal', 'large'), 'default' => 'normal'),
|
||||
'font_weight' => array('type' => 'enum', 'allowed' => array('400', '500', '600', '700'), 'default' => '500'),
|
||||
'box_shadow_intensity' => array('type' => 'enum', 'allowed' => array('none', 'light', 'normal', 'strong'), 'default' => 'normal'),
|
||||
'border_radius' => array('type' => 'int', 'default' => 4),
|
||||
'padding_vertical' => array('type' => 'float', 'default' => 0.75),
|
||||
'link_padding_vertical' => array('type' => 'float', 'default' => 0.5),
|
||||
'link_padding_horizontal' => array('type' => 'float', 'default' => 0.65),
|
||||
'z_index' => array('type' => 'int', 'default' => 1030),
|
||||
'transition_speed' => array('type' => 'enum', 'allowed' => array('fast', 'normal', 'slow'), 'default' => 'normal')
|
||||
)))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Sanitizer Helper
|
||||
*
|
||||
* Métodos estáticos reutilizables para sanitización de datos
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @subpackage Admin_Panel\Sanitizers
|
||||
* @since 2.1.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class APUS_Sanitizer_Helper
|
||||
*
|
||||
* Proporciona métodos estáticos para sanitización común,
|
||||
* eliminando código duplicado en los sanitizadores de componentes
|
||||
*/
|
||||
class APUS_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,88 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Top Bar Sanitizer
|
||||
*
|
||||
* Sanitiza configuraciones del componente Top Bar
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @subpackage Admin_Panel\Sanitizers
|
||||
* @since 2.1.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class APUS_TopBar_Sanitizer
|
||||
*
|
||||
* Sanitiza todas las configuraciones del componente Top Bar
|
||||
*/
|
||||
class APUS_TopBar_Sanitizer {
|
||||
|
||||
/**
|
||||
* Obtiene los valores por defecto del Top Bar
|
||||
*
|
||||
* @return array Valores por defecto
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public function get_defaults() {
|
||||
return array(
|
||||
'enabled' => true,
|
||||
'show_on_mobile' => true,
|
||||
'show_on_desktop' => true,
|
||||
'icon_class' => 'bi bi-megaphone-fill',
|
||||
'show_icon' => true,
|
||||
'highlight_text' => 'Nuevo:',
|
||||
'message_text' => 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
|
||||
'link_text' => 'Ver Catálogo',
|
||||
'link_url' => '/catalogo',
|
||||
'link_target' => '_self',
|
||||
'show_link' => true,
|
||||
'custom_styles' => array(
|
||||
'background_color' => '#0E2337',
|
||||
'text_color' => '#ffffff',
|
||||
'highlight_color' => '#FF8600',
|
||||
'link_hover_color' => '#FF8600',
|
||||
'font_size' => 'normal'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza los datos del Top Bar
|
||||
*
|
||||
* @param array $data Datos sin sanitizar del Top Bar
|
||||
* @return array Datos sanitizados
|
||||
*/
|
||||
public function sanitize($data) {
|
||||
return array_merge(
|
||||
// Booleanos
|
||||
APUS_Sanitizer_Helper::sanitize_booleans($data, array(
|
||||
'enabled', 'show_on_mobile', 'show_on_desktop', 'show_icon', 'show_link'
|
||||
)),
|
||||
|
||||
// Textos
|
||||
APUS_Sanitizer_Helper::sanitize_texts($data, array(
|
||||
'icon_class', 'highlight_text', 'message_text', 'link_text'
|
||||
)),
|
||||
|
||||
// URL
|
||||
array('link_url' => APUS_Sanitizer_Helper::sanitize_url($data, 'link_url')),
|
||||
|
||||
// Enum
|
||||
array('link_target' => APUS_Sanitizer_Helper::sanitize_enum(
|
||||
$data, 'link_target', array('_self', '_blank'), '_self'
|
||||
)),
|
||||
|
||||
// Custom styles anidado
|
||||
array('custom_styles' => APUS_Sanitizer_Helper::sanitize_nested_group($data, 'custom_styles', array(
|
||||
'background_color' => array('type' => 'color', 'default' => ''),
|
||||
'text_color' => array('type' => 'color', 'default' => ''),
|
||||
'highlight_color' => array('type' => 'color', 'default' => ''),
|
||||
'link_hover_color' => array('type' => 'color', 'default' => ''),
|
||||
'font_size' => array('type' => 'enum', 'allowed' => array('small', 'normal', 'large'), 'default' => 'normal')
|
||||
)))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Panel Module - Initialization
|
||||
*
|
||||
* Sistema de configuración por componentes
|
||||
* Cada componente del tema es configurable desde el admin panel
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Module constants
|
||||
define('APUS_ADMIN_PANEL_VERSION', '2.1.4');
|
||||
define('APUS_ADMIN_PANEL_PATH', get_template_directory() . '/admin/');
|
||||
define('APUS_ADMIN_PANEL_URL', get_template_directory_uri() . '/admin/');
|
||||
|
||||
// Load classes
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/class-admin-menu.php';
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/class-db-manager.php';
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/class-data-migrator.php';
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/class-validator.php';
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/class-theme-options-migrator.php';
|
||||
|
||||
// Load sanitizer helper (DRY - @since 2.1.0)
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/sanitizers/class-sanitizer-helper.php';
|
||||
|
||||
// Load sanitizers (Strategy Pattern - @since 2.1.0)
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/sanitizers/class-topbar-sanitizer.php';
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/sanitizers/class-navbar-sanitizer.php';
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/sanitizers/class-letstalkbutton-sanitizer.php';
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/sanitizers/class-herosection-sanitizer.php';
|
||||
|
||||
// Settings Manager (debe cargarse DESPUÉS de sanitizers)
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'includes/class-settings-manager.php';
|
||||
|
||||
// Initialize Database Manager
|
||||
new APUS_DB_Manager();
|
||||
|
||||
// Execute data migration (one-time operation)
|
||||
add_action('admin_init', function() {
|
||||
$migrator = new APUS_Data_Migrator();
|
||||
$result = $migrator->maybe_migrate();
|
||||
|
||||
if ($result['success'] && isset($result['total_migrated'])) {
|
||||
error_log('APUS Theme: Migración completada - ' . $result['total_migrated'] . ' registros migrados');
|
||||
}
|
||||
});
|
||||
|
||||
// Execute Theme Options migration (one-time operation)
|
||||
add_action('admin_init', function() {
|
||||
$theme_options_migrator = new APUS_Theme_Options_Migrator();
|
||||
|
||||
// Solo ejecutar si no se ha migrado ya
|
||||
if (!$theme_options_migrator->is_migrated()) {
|
||||
$result = $theme_options_migrator->migrate();
|
||||
|
||||
if ($result['success']) {
|
||||
error_log('APUS Theme: Theme Options migradas exitosamente - ' . $result['migrated'] . ' configuraciones');
|
||||
} else {
|
||||
error_log('APUS Theme: Error en migración de Theme Options - ' . $result['message']);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,530 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Panel - Main Page
|
||||
*
|
||||
* Interfaz de administración de componentes del tema
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap apus-admin-panel">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
<p class="description">Configure los componentes del tema Apus</p>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" data-bs-target="#topBarTab" href="#topBarTab" role="tab">
|
||||
<i class="bi bi-megaphone-fill me-2"></i>
|
||||
Top Bar
|
||||
</a>
|
||||
</li>
|
||||
<!-- Más tabs aquí: Navbar, Hero, Footer, etc. -->
|
||||
</ul>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<!-- ============================= -->
|
||||
<!-- TAB: TOP BAR - VERSIÓN MEJORADA -->
|
||||
<!-- ============================= -->
|
||||
<div id="topBarTab" class="tab-pane fade show active" role="tabpanel">
|
||||
<!-- Header del Tab -->
|
||||
<div class="tab-header mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="mb-1 text-navy-primary">
|
||||
<i class="bi bi-megaphone-fill me-2 text-orange-primary"></i>
|
||||
Configuración Top Bar
|
||||
</h3>
|
||||
<p class="text-neutral-600 mb-0">
|
||||
Personaliza la barra de anuncios superior de tu sitio
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="resetTopBarDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row para 2 cards por fila -->
|
||||
<div class="row">
|
||||
<!-- ============================= -->
|
||||
<!-- GRUPO 1: ACTIVACIÓN -->
|
||||
<!-- ============================= -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-section card shadow-sm border-0 mb-4 h-100">
|
||||
<div class="card-body">
|
||||
<h4 class="section-title">
|
||||
<span class="title-icon">
|
||||
<i class="bi bi-toggle-on"></i>
|
||||
</span>
|
||||
Activación y Visibilidad
|
||||
</h4>
|
||||
|
||||
<!-- Enabled -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-neutral-700 fw-medium mb-3">
|
||||
<i class="bi bi-power text-orange-primary me-1"></i>
|
||||
Estado del Componente
|
||||
</label>
|
||||
<div class="toggle-container">
|
||||
<div class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="topBarEnabled"
|
||||
role="switch"
|
||||
checked>
|
||||
<label class="form-check-label" for="topBarEnabled">
|
||||
Activar Top Bar
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-neutral-700 d-block mt-2">
|
||||
Activa o desactiva el Top Bar en todo el sitio
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show on Mobile -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-neutral-700 fw-medium mb-3">
|
||||
<i class="bi bi-phone text-orange-primary me-1"></i>
|
||||
Visibilidad Mobile
|
||||
</label>
|
||||
<div class="toggle-container">
|
||||
<div class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="topBarShowOnMobile"
|
||||
role="switch"
|
||||
checked>
|
||||
<label class="form-check-label" for="topBarShowOnMobile">
|
||||
Mostrar en dispositivos móviles
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-neutral-700 d-block mt-2">
|
||||
Pantallas menores a 768px
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show on Desktop -->
|
||||
<div class="mb-0">
|
||||
<label class="form-label text-neutral-700 fw-medium mb-3">
|
||||
<i class="bi bi-display text-orange-primary me-1"></i>
|
||||
Visibilidad Desktop
|
||||
</label>
|
||||
<div class="toggle-container">
|
||||
<div class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="topBarShowOnDesktop"
|
||||
role="switch"
|
||||
checked>
|
||||
<label class="form-check-label" for="topBarShowOnDesktop">
|
||||
Mostrar en desktop
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-neutral-700 d-block mt-2">
|
||||
Pantallas de 768px en adelante
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- ============================= -->
|
||||
<!-- GRUPO 2: CONTENIDO -->
|
||||
<!-- ============================= -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-section card shadow-sm border-0 mb-4 h-100">
|
||||
<div class="card-body">
|
||||
<h4 class="section-title">
|
||||
<span class="title-icon">
|
||||
<i class="bi bi-card-text"></i>
|
||||
</span>
|
||||
Contenido y Mensajes
|
||||
</h4>
|
||||
|
||||
<!-- Icono -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<label for="topBarIconClass" class="form-label text-neutral-700 fw-medium">
|
||||
<i class="bi bi-emoji-smile text-orange-primary me-1"></i>
|
||||
Clase del icono
|
||||
<span class="badge bg-neutral-100 text-neutral-600 ms-2">Bootstrap Icons</span>
|
||||
</label>
|
||||
<div class="input-group input-group-merge">
|
||||
<span class="input-group-text bg-neutral-50 border-end-0">
|
||||
<i class="bi bi-code-slash text-neutral-600"></i>
|
||||
</span>
|
||||
<input type="text"
|
||||
id="topBarIconClass"
|
||||
class="form-control border-start-0 ps-0"
|
||||
placeholder="Ej: bi bi-megaphone-fill"
|
||||
maxlength="50"
|
||||
value="bi bi-megaphone-fill">
|
||||
</div>
|
||||
<small class="form-text text-neutral-700 d-flex align-items-center mt-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Ver iconos disponibles:
|
||||
<a href="https://icons.getbootstrap.com/" target="_blank" class="ms-1 text-orange-primary">
|
||||
Bootstrap Icons <i class="bi bi-box-arrow-up-right ms-1"></i>
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label class="form-label text-neutral-700 fw-medium mb-3">
|
||||
Opciones de Icono
|
||||
</label>
|
||||
<div class="toggle-container">
|
||||
<div class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="topBarShowIcon"
|
||||
role="switch"
|
||||
checked>
|
||||
<label class="form-check-label" for="topBarShowIcon">
|
||||
Mostrar icono
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texto destacado -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label for="topBarHighlightText" class="form-label text-neutral-700 fw-medium">
|
||||
<i class="bi bi-bookmark-star text-orange-primary me-1"></i>
|
||||
Texto destacado
|
||||
<span class="badge bg-warning-subtle text-warning-emphasis ms-2">Opcional</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="topBarHighlightText"
|
||||
class="form-control form-control-lg"
|
||||
placeholder='Ej: "Nuevo:" o "Promoción:"'
|
||||
maxlength="30"
|
||||
value="Nuevo:">
|
||||
<small class="form-text text-neutral-700 d-flex align-items-center mt-2">
|
||||
<i class="bi bi-lightbulb text-warning me-1"></i>
|
||||
Se muestra en <strong class="mx-1">negritas</strong> y con <span class="text-orange-primary fw-bold mx-1">color destacado</span>. Dejar vacío para omitir.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mensaje principal -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label for="topBarMessageText" class="form-label text-neutral-700 fw-medium">
|
||||
<i class="bi bi-chat-left-text text-orange-primary me-1"></i>
|
||||
Mensaje principal
|
||||
<span class="badge bg-danger-subtle text-danger-emphasis ms-2">Requerido</span>
|
||||
</label>
|
||||
<textarea id="topBarMessageText"
|
||||
class="form-control form-control-lg"
|
||||
rows="3"
|
||||
maxlength="250"
|
||||
placeholder="Ej: Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025."
|
||||
required>Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</textarea>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
<small class="form-text text-neutral-700">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Mensaje que se mostrará en la barra superior
|
||||
</small>
|
||||
<small class="form-text">
|
||||
<span id="topBarMessageTextCount" class="fw-bold">75</span><span class="text-neutral-600">/250 caracteres</span>
|
||||
</small>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 4px;">
|
||||
<div id="topBarMessageTextProgress"
|
||||
class="progress-bar bg-orange-primary"
|
||||
role="progressbar"
|
||||
style="width: 30%"
|
||||
aria-valuenow="75"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="250"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enlace -->
|
||||
<div class="row g-4">
|
||||
<div class="col-md-5">
|
||||
<div class="form-group">
|
||||
<label for="topBarLinkText" class="form-label text-neutral-700 fw-medium">
|
||||
<i class="bi bi-link-45deg text-orange-primary me-1"></i>
|
||||
Texto del enlace
|
||||
</label>
|
||||
<input type="text"
|
||||
id="topBarLinkText"
|
||||
class="form-control"
|
||||
placeholder="Ej: Ver Catálogo"
|
||||
maxlength="50"
|
||||
value="Ver Catálogo →">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<div class="form-group">
|
||||
<label for="topBarLinkUrl" class="form-label text-neutral-700 fw-medium">
|
||||
<i class="bi bi-globe text-orange-primary me-1"></i>
|
||||
URL del enlace
|
||||
</label>
|
||||
<input type="url"
|
||||
id="topBarLinkUrl"
|
||||
class="form-control"
|
||||
placeholder="Ej: /catalogo o https://ejemplo.com"
|
||||
value="/catalogo">
|
||||
<small class="form-text text-neutral-700 mt-2 d-block">
|
||||
URLs relativas (/page) o absolutas (https://...)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label for="topBarLinkTarget" class="form-label text-neutral-700 fw-medium">
|
||||
<i class="bi bi-window text-orange-primary me-1"></i>
|
||||
Target
|
||||
</label>
|
||||
<select id="topBarLinkTarget" class="form-select">
|
||||
<option value="_self" selected>Misma ventana</option>
|
||||
<option value="_blank">Nueva ventana</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<div class="toggle-container">
|
||||
<div class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
id="topBarShowLink"
|
||||
role="switch"
|
||||
checked>
|
||||
<label class="form-check-label" for="topBarShowLink">
|
||||
<strong>Mostrar enlace</strong>
|
||||
<span class="text-neutral-700 ms-2">- Activa para incluir un botón de acción</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row para segunda fila de cards -->
|
||||
<div class="row">
|
||||
<!-- ============================= -->
|
||||
<!-- GRUPO 3: ESTILOS AVANZADOS -->
|
||||
<!-- ============================= -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-section card shadow-sm border-0 mb-4 h-100">
|
||||
<div class="card-body">
|
||||
<h4 class="section-title">
|
||||
<span class="title-icon">
|
||||
<i class="bi bi-palette"></i>
|
||||
</span>
|
||||
Estilos Personalizados
|
||||
</h4>
|
||||
|
||||
<!-- Colores (4 en una fila) -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="topBarBgColor" class="form-label text-neutral-700 fw-medium mb-3">
|
||||
<i class="bi bi-paint-bucket text-orange-primary me-1"></i>
|
||||
Color de fondo
|
||||
</label>
|
||||
<div class="color-picker-wrapper">
|
||||
<input type="color"
|
||||
id="topBarBgColor"
|
||||
class="form-control form-control-color"
|
||||
value="#0E2337"
|
||||
title="Seleccionar color de fondo">
|
||||
<div class="color-preview-text mt-2">
|
||||
<code class="text-neutral-600 small">#0E2337</code>
|
||||
<small class="text-neutral-700 d-block">Navy Dark (Default)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="topBarTextColor" class="form-label text-neutral-700 fw-medium mb-3">
|
||||
<i class="bi bi-fonts text-orange-primary me-1"></i>
|
||||
Color de texto
|
||||
</label>
|
||||
<div class="color-picker-wrapper">
|
||||
<input type="color"
|
||||
id="topBarTextColor"
|
||||
class="form-control form-control-color"
|
||||
value="#ffffff"
|
||||
title="Seleccionar color de texto">
|
||||
<div class="color-preview-text mt-2">
|
||||
<code class="text-neutral-600 small">#ffffff</code>
|
||||
<small class="text-neutral-700 d-block">White (Default)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="topBarHighlightColor" class="form-label text-neutral-700 fw-medium mb-3">
|
||||
<i class="bi bi-star text-orange-primary me-1"></i>
|
||||
Color destacado
|
||||
</label>
|
||||
<div class="color-picker-wrapper">
|
||||
<input type="color"
|
||||
id="topBarHighlightColor"
|
||||
class="form-control form-control-color"
|
||||
value="#FF8600"
|
||||
title="Seleccionar color destacado">
|
||||
<div class="color-preview-text mt-2">
|
||||
<code class="text-neutral-600 small">#FF8600</code>
|
||||
<small class="text-neutral-700 d-block">Orange Primary (Default)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label for="topBarLinkHoverColor" class="form-label text-neutral-700 fw-medium mb-3">
|
||||
<i class="bi bi-cursor text-orange-primary me-1"></i>
|
||||
Hover enlace
|
||||
</label>
|
||||
<div class="color-picker-wrapper">
|
||||
<input type="color"
|
||||
id="topBarLinkHoverColor"
|
||||
class="form-control form-control-color"
|
||||
value="#FF6B35"
|
||||
title="Seleccionar color hover del enlace">
|
||||
<div class="color-preview-text mt-2">
|
||||
<code class="text-neutral-600 small">#FF6B35</code>
|
||||
<small class="text-neutral-700 d-block">Orange Hover (Default)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tamaño de fuente -->
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="topBarFontSize" class="form-label text-neutral-700 fw-medium">
|
||||
<i class="bi bi-type text-orange-primary me-1"></i>
|
||||
Tamaño de fuente
|
||||
</label>
|
||||
<select id="topBarFontSize" class="form-select form-select-lg">
|
||||
<option value="small">
|
||||
Pequeño - 0.8rem (ideal para mucho texto)
|
||||
</option>
|
||||
<option value="normal" selected>
|
||||
Normal - 0.9rem (recomendado)
|
||||
</option>
|
||||
<option value="large">
|
||||
Grande - 1rem (máxima legibilidad)
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-neutral-700 d-block mt-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
El tamaño afecta la altura total de la barra
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-info-custom d-flex align-items-start" role="alert">
|
||||
<i class="bi bi-lightbulb-fill text-orange-primary me-3 fs-4"></i>
|
||||
<div>
|
||||
<h6 class="alert-heading mb-1">Tip de Diseño</h6>
|
||||
<p class="mb-0 small">
|
||||
Para mejor legibilidad, usa <strong>fondos oscuros</strong> (Navy Dark) con <strong>texto claro</strong> (White)
|
||||
y <strong>acentos naranjas</strong> para las acciones importantes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- VISTA PREVIA INTERACTIVA -->
|
||||
<!-- ============================= -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-section card shadow-sm border-0 mb-4 bg-neutral-50 h-100">
|
||||
<div class="card-body">
|
||||
<h4 class="section-title">
|
||||
<span class="title-icon">
|
||||
<i class="bi bi-eye"></i>
|
||||
</span>
|
||||
Vista Previa en Tiempo Real
|
||||
</h4>
|
||||
|
||||
<div class="preview-container border border-2 border-neutral-100 rounded-3 p-4 bg-white">
|
||||
<!-- Top Bar Preview -->
|
||||
<div id="topBarPreview" class="top-bar-preview" style="background-color: #0E2337; color: #ffffff; padding: 12px 20px; border-radius: 8px; display: flex; align-items: center; justify-content: center; gap: 15px; flex-wrap: wrap;">
|
||||
<i class="bi bi-megaphone-fill" style="font-size: 1.2rem; color: #FF8600;"></i>
|
||||
<span style="font-weight: 700; color: #FF8600;">Nuevo:</span>
|
||||
<span style="flex: 1; min-width: 300px; text-align: center;">Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</span>
|
||||
<a href="#" style="color: #ffffff; text-decoration: underline; white-space: nowrap; transition: color 0.3s;">Ver Catálogo →</a>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<small class="text-neutral-700">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
La vista previa se actualiza automáticamente al modificar los campos
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2 justify-content-end">
|
||||
<button type="button" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-phone me-1"></i>
|
||||
Ver en Mobile
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-display me-1"></i>
|
||||
Ver en Desktop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="admin-actions mt-4">
|
||||
<button type="button" id="saveSettings" class="button button-primary" disabled>
|
||||
<i class="bi bi-save me-2"></i>Guardar Cambios
|
||||
</button>
|
||||
<span class="spinner" style="display: none; float: none; margin-left: 10px;"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,281 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Panel - Theme Options Migration Page
|
||||
*
|
||||
* Interfaz para migrar Theme Options de wp_options a tabla personalizada
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Instanciar migrator
|
||||
$migrator = new APUS_Theme_Options_Migrator();
|
||||
|
||||
// Obtener estadísticas
|
||||
$stats = $migrator->get_migration_stats();
|
||||
|
||||
// Procesar acciones
|
||||
$message = '';
|
||||
$message_type = '';
|
||||
|
||||
if (isset($_POST['apus_migrate_action'])) {
|
||||
check_admin_referer('apus_migration_action', 'apus_migration_nonce');
|
||||
|
||||
$action = sanitize_text_field($_POST['apus_migrate_action']);
|
||||
|
||||
switch ($action) {
|
||||
case 'migrate':
|
||||
$result = $migrator->migrate();
|
||||
$message = $result['message'];
|
||||
$message_type = $result['success'] ? 'success' : 'error';
|
||||
|
||||
// Actualizar estadísticas
|
||||
$stats = $migrator->get_migration_stats();
|
||||
break;
|
||||
|
||||
case 'rollback':
|
||||
$backup_name = isset($_POST['backup_name']) ? sanitize_text_field($_POST['backup_name']) : null;
|
||||
$result = $migrator->rollback($backup_name);
|
||||
$message = $result['message'];
|
||||
$message_type = $result['success'] ? 'success' : 'error';
|
||||
|
||||
// Actualizar estadísticas
|
||||
$stats = $migrator->get_migration_stats();
|
||||
break;
|
||||
|
||||
case 'delete_backup':
|
||||
$backup_name = isset($_POST['backup_name']) ? sanitize_text_field($_POST['backup_name']) : '';
|
||||
if ($backup_name && $migrator->delete_backup($backup_name)) {
|
||||
$message = 'Backup eliminado correctamente';
|
||||
$message_type = 'success';
|
||||
} else {
|
||||
$message = 'Error al eliminar backup';
|
||||
$message_type = 'error';
|
||||
}
|
||||
|
||||
// Actualizar estadísticas
|
||||
$stats = $migrator->get_migration_stats();
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
<p class="description">Migración de Theme Options desde wp_options a tabla personalizada wp_apus_theme_components</p>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="notice notice-<?php echo esc_attr($message_type); ?> is-dismissible">
|
||||
<p><?php echo esc_html($message); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Migration Status Card -->
|
||||
<div class="card mt-4" style="max-width: 800px;">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Estado de la Migración
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<strong>Estado:</strong>
|
||||
<?php if ($stats['is_migrated']): ?>
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Migrado
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning text-dark">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Pendiente
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<strong>Backups disponibles:</strong>
|
||||
<span class="badge bg-info"><?php echo esc_html($stats['backups_count']); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<strong>Opciones en wp_options:</strong>
|
||||
<span class="badge bg-secondary"><?php echo esc_html($stats['old_options_count']); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<strong>Configs en tabla nueva:</strong>
|
||||
<span class="badge bg-primary"><?php echo esc_html($stats['new_config_count']); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar (si hay migración parcial) -->
|
||||
<?php if (!$stats['is_migrated'] && $stats['new_config_count'] > 0): ?>
|
||||
<div class="progress mt-3" style="height: 25px;">
|
||||
<?php
|
||||
$total = max($stats['old_options_count'], $stats['new_config_count']);
|
||||
$percentage = $total > 0 ? ($stats['new_config_count'] / $total) * 100 : 0;
|
||||
?>
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
style="width: <?php echo esc_attr($percentage); ?>%;"
|
||||
aria-valuenow="<?php echo esc_attr($percentage); ?>"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
<?php echo esc_html(round($percentage, 1)); ?>%
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">Migración parcial detectada</small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons Card -->
|
||||
<div class="card mt-4" style="max-width: 800px;">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-gear me-2"></i>
|
||||
Acciones
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!$stats['is_migrated']): ?>
|
||||
<!-- Migrate Button -->
|
||||
<form method="post" style="display: inline;">
|
||||
<?php wp_nonce_field('apus_migration_action', 'apus_migration_nonce'); ?>
|
||||
<input type="hidden" name="apus_migrate_action" value="migrate">
|
||||
<button type="submit" class="btn btn-primary" onclick="return confirm('¿Está seguro de ejecutar la migración? Se creará un backup automático.');">
|
||||
<i class="bi bi-arrow-right-circle me-1"></i>
|
||||
Ejecutar Migración
|
||||
</button>
|
||||
</form>
|
||||
<p class="text-muted mt-2 mb-0">
|
||||
<small>
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Se creará un backup automático antes de la migración. Total de configuraciones: <?php echo esc_html($stats['old_options_count']); ?>
|
||||
</small>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-success mb-0">
|
||||
<i class="bi bi-check-circle me-2"></i>
|
||||
La migración ya ha sido completada. Las opciones del tema ahora se leen desde la tabla personalizada.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backups Card -->
|
||||
<?php if ($stats['backups_count'] > 0): ?>
|
||||
<div class="card mt-4" style="max-width: 800px;">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-archive me-2"></i>
|
||||
Backups Disponibles (<?php echo esc_html($stats['backups_count']); ?>)
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre del Backup</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($stats['backups'] as $backup): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<code><?php echo esc_html($backup); ?></code>
|
||||
</td>
|
||||
<td>
|
||||
<!-- Rollback -->
|
||||
<form method="post" style="display: inline;" class="me-2">
|
||||
<?php wp_nonce_field('apus_migration_action', 'apus_migration_nonce'); ?>
|
||||
<input type="hidden" name="apus_migrate_action" value="rollback">
|
||||
<input type="hidden" name="backup_name" value="<?php echo esc_attr($backup); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-warning" onclick="return confirm('¿Está seguro de restaurar este backup? Esto revertirá la migración.');">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Delete -->
|
||||
<form method="post" style="display: inline;">
|
||||
<?php wp_nonce_field('apus_migration_action', 'apus_migration_nonce'); ?>
|
||||
<input type="hidden" name="apus_migrate_action" value="delete_backup">
|
||||
<input type="hidden" name="backup_name" value="<?php echo esc_attr($backup); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('¿Está seguro de eliminar este backup?');">
|
||||
<i class="bi bi-trash me-1"></i>
|
||||
Eliminar
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Technical Information -->
|
||||
<div class="card mt-4" style="max-width: 800px;">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-code-square me-2"></i>
|
||||
Información Técnica
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">Componente:</dt>
|
||||
<dd class="col-sm-8"><code>theme</code></dd>
|
||||
|
||||
<dt class="col-sm-4">Tabla antigua:</dt>
|
||||
<dd class="col-sm-8"><code>wp_options</code> (opción: <code>apus_theme_options</code>)</dd>
|
||||
|
||||
<dt class="col-sm-4">Tabla nueva:</dt>
|
||||
<dd class="col-sm-8"><code>wp_apus_theme_components</code></dd>
|
||||
|
||||
<dt class="col-sm-4">Versión Admin Panel:</dt>
|
||||
<dd class="col-sm-8"><code><?php echo esc_html(APUS_ADMIN_PANEL_VERSION); ?></code></dd>
|
||||
|
||||
<dt class="col-sm-4">Archivo Helper:</dt>
|
||||
<dd class="col-sm-8"><code>inc/theme-settings.php</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user