Migración completa a Clean Architecture con componentes funcionales

- Reorganización de estructura: Admin/, Public/, Shared/, Schemas/
- 12 componentes migrados: TopNotificationBar, Navbar, CtaLetsTalk, Hero,
  FeaturedImage, TableOfContents, CtaBoxSidebar, SocialShare, CtaPost,
  RelatedPost, ContactForm, Footer
- Panel de administración con tabs Bootstrap 5 funcionales
- Schemas JSON para configuración de componentes
- Renderers dinámicos con CSSGeneratorService (cero CSS hardcodeado)
- FormBuilders para UI admin con Design System consistente
- Fix: Bootstrap JS cargado en header para tabs funcionales
- Fix: buildTextInput maneja valores mixed (bool/string)
- Eliminación de estructura legacy (src/, admin/, assets/css/componente-*)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-25 21:20:06 -06:00
parent 90de6df77c
commit 0846a3bf03
224 changed files with 21670 additions and 17816 deletions

View 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 '';
}
}