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:
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 '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user