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,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;
}
}

View File

@@ -0,0 +1,461 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* ContactFormRenderer - Renderiza formulario de contacto con webhook
*
* RESPONSABILIDAD: Generar HTML y CSS del formulario de contacto
*
* CARACTERISTICAS:
* - Formulario responsive Bootstrap 5
* - Envio a webhook configurable (no expuesto en frontend)
* - Info de contacto configurable
* - Mensajes de exito/error personalizables
*
* @package ROITheme\Public\ContactForm\Infrastructure\Ui
*/
final class ContactFormRenderer implements RendererInterface
{
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
$visibilityClass = $this->getVisibilityClass($data);
if ($visibilityClass === null) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data, $visibilityClass);
$js = $this->buildJS($data);
return sprintf("<style>%s</style>\n%s\n<script>%s</script>", $css, $html, $js);
}
public function supports(string $componentType): bool
{
return $componentType === 'contact-form';
}
private function isEnabled(array $data): bool
{
$value = $data['visibility']['is_enabled'] ?? false;
return $value === true || $value === '1' || $value === 1;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
switch ($showOn) {
case 'all':
return true;
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function getVisibilityClass(array $data): ?string
{
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
$showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
$showMobile = $data['visibility']['show_on_mobile'] ?? true;
$showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
if (!$showDesktop && !$showMobile) {
return null;
}
if (!$showDesktop && $showMobile) {
return 'd-lg-none';
}
if ($showDesktop && !$showMobile) {
return 'd-none d-lg-block';
}
return '';
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$spacing = $data['spacing'] ?? [];
$effects = $data['visual_effects'] ?? [];
$cssRules = [];
// Section background
$sectionBgColor = $colors['section_bg_color'] ?? 'rgba(108, 117, 125, 0.25)';
$sectionPaddingY = $spacing['section_padding_y'] ?? '3rem';
$sectionMarginTop = $spacing['section_margin_top'] ?? '3rem';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section', [
'background-color' => $sectionBgColor,
'padding-top' => $sectionPaddingY,
'padding-bottom' => $sectionPaddingY,
'margin-top' => $sectionMarginTop,
]);
// Title
$titleColor = $colors['title_color'] ?? '#212529';
$titleMarginBottom = $spacing['title_margin_bottom'] ?? '0.75rem';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-title', [
'color' => $titleColor,
'margin-bottom' => $titleMarginBottom,
]);
// Description
$descColor = $colors['description_color'] ?? '#212529';
$descMarginBottom = $spacing['description_margin_bottom'] ?? '1.5rem';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-description', [
'color' => $descColor,
'margin-bottom' => $descMarginBottom,
]);
// Icons
$iconColor = $colors['icon_color'] ?? '#FF8600';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-icon', [
'color' => $iconColor,
]);
// Info labels and values
$infoLabelColor = $colors['info_label_color'] ?? '#212529';
$infoValueColor = $colors['info_value_color'] ?? '#6c757d';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-label', [
'color' => $infoLabelColor,
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-value', [
'color' => $infoValueColor,
]);
// Form inputs
$inputBorderColor = $colors['input_border_color'] ?? '#dee2e6';
$inputFocusBorder = $colors['input_focus_border'] ?? '#FF8600';
$inputBorderRadius = $effects['input_border_radius'] ?? '6px';
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control', [
'border-color' => $inputBorderColor,
'border-radius' => $inputBorderRadius,
'transition' => "all {$transitionDuration} ease",
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control:focus', [
'border-color' => $inputFocusBorder,
'box-shadow' => "0 0 0 0.2rem rgba(255, 134, 0, 0.25)",
'outline' => 'none',
]);
// Submit button
$buttonBgColor = $colors['button_bg_color'] ?? '#FF8600';
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
$buttonHoverBg = $colors['button_hover_bg'] ?? '#e67a00';
$buttonBorderRadius = $effects['button_border_radius'] ?? '6px';
$buttonPadding = $effects['button_padding'] ?? '0.75rem 2rem';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit', [
'background-color' => $buttonBgColor,
'color' => $buttonTextColor,
'font-weight' => '600',
'padding' => $buttonPadding,
'border' => 'none',
'border-radius' => $buttonBorderRadius,
'transition' => "all {$transitionDuration} ease",
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:hover', [
'background-color' => $buttonHoverBg,
'color' => $buttonTextColor,
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:disabled', [
'opacity' => '0.7',
'cursor' => 'not-allowed',
]);
// Success/Error messages
$successBgColor = $colors['success_bg_color'] ?? '#d1e7dd';
$successTextColor = $colors['success_text_color'] ?? '#0f5132';
$errorBgColor = $colors['error_bg_color'] ?? '#f8d7da';
$errorTextColor = $colors['error_text_color'] ?? '#842029';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-success', [
'background-color' => $successBgColor,
'color' => $successTextColor,
'border-color' => $successBgColor,
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-danger', [
'background-color' => $errorBgColor,
'color' => $errorTextColor,
'border-color' => $errorBgColor,
]);
return implode("\n", $cssRules);
}
private function buildHTML(array $data, string $visibilityClass): string
{
$content = $data['content'] ?? [];
$contactInfo = $data['contact_info'] ?? [];
$formLabels = $data['form_labels'] ?? [];
$effects = $data['visual_effects'] ?? [];
// Content
$sectionTitle = $content['section_title'] ?? '¿Tienes alguna pregunta?';
$sectionDesc = $content['section_description'] ?? 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.';
$submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
$submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
// Contact info
$showContactInfo = $contactInfo['show_contact_info'] ?? true;
$showContactInfo = $showContactInfo === true || $showContactInfo === '1' || $showContactInfo === 1;
// Form labels/placeholders
$fullnamePlaceholder = $formLabels['fullname_placeholder'] ?? 'Nombre completo *';
$companyPlaceholder = $formLabels['company_placeholder'] ?? 'Empresa';
$whatsappPlaceholder = $formLabels['whatsapp_placeholder'] ?? 'WhatsApp *';
$emailPlaceholder = $formLabels['email_placeholder'] ?? 'Correo electrónico *';
$messagePlaceholder = $formLabels['message_placeholder'] ?? '¿En qué podemos ayudarte?';
$textareaRows = $effects['textarea_rows'] ?? '4';
// Container class
$containerClass = 'roi-contact-form-section';
if (!empty($visibilityClass)) {
$containerClass .= ' ' . $visibilityClass;
}
// Nonce for AJAX security
$nonce = wp_create_nonce('roi_contact_form_nonce');
$html = sprintf('<section class="%s">', esc_attr($containerClass));
$html .= '<div class="container">';
$html .= '<div class="row justify-content-center">';
$html .= '<div class="col-lg-10">';
$html .= '<div class="row">';
// Left column - Contact info
$html .= '<div class="col-lg-5 mb-4 mb-lg-0">';
$html .= sprintf('<h2 class="h3 contact-title">%s</h2>', esc_html($sectionTitle));
$html .= sprintf('<p class="contact-description">%s</p>', esc_html($sectionDesc));
if ($showContactInfo) {
$html .= $this->buildContactInfoHTML($contactInfo);
}
$html .= '</div>';
// Right column - Form
$html .= '<div class="col-lg-7">';
$html .= sprintf('<form id="roiContactForm" data-nonce="%s">', esc_attr($nonce));
$html .= '<div class="row g-3">';
// Full name field
$html .= '<div class="col-md-6">';
$html .= sprintf(
'<input type="text" class="form-control" id="roiContactFullName" name="fullName" placeholder="%s" required>',
esc_attr($fullnamePlaceholder)
);
$html .= '</div>';
// Company field
$html .= '<div class="col-md-6">';
$html .= sprintf(
'<input type="text" class="form-control" id="roiContactCompany" name="company" placeholder="%s">',
esc_attr($companyPlaceholder)
);
$html .= '</div>';
// WhatsApp field
$html .= '<div class="col-md-6">';
$html .= sprintf(
'<input type="tel" class="form-control" id="roiContactWhatsapp" name="whatsapp" placeholder="%s" required>',
esc_attr($whatsappPlaceholder)
);
$html .= '</div>';
// Email field
$html .= '<div class="col-md-6">';
$html .= sprintf(
'<input type="email" class="form-control" id="roiContactEmail" name="email" placeholder="%s" required>',
esc_attr($emailPlaceholder)
);
$html .= '</div>';
// Message field
$html .= '<div class="col-12">';
$html .= sprintf(
'<textarea class="form-control" id="roiContactMessage" name="message" rows="%s" placeholder="%s"></textarea>',
esc_attr($textareaRows),
esc_attr($messagePlaceholder)
);
$html .= '</div>';
// Submit button
$html .= '<div class="col-12">';
$html .= '<button type="submit" class="btn btn-contact-submit w-100">';
$html .= sprintf('<i class="%s me-2"></i>', esc_attr($submitIcon));
$html .= esc_html($submitText);
$html .= '</button>';
$html .= '</div>';
// Message container
$html .= '<div id="roiContactFormMessage" class="col-12 mt-2 alert" style="display: none;"></div>';
$html .= '</div>'; // .row g-3
$html .= '</form>';
$html .= '</div>'; // .col-lg-7
$html .= '</div>'; // .row
$html .= '</div>'; // .col-lg-10
$html .= '</div>'; // .row justify-content-center
$html .= '</div>'; // .container
$html .= '</section>';
return $html;
}
private function buildContactInfoHTML(array $contactInfo): string
{
$phoneLabel = $contactInfo['phone_label'] ?? 'Teléfono';
$phoneValue = $contactInfo['phone_value'] ?? '+52 55 1234 5678';
$emailLabel = $contactInfo['email_label'] ?? 'Email';
$emailValue = $contactInfo['email_value'] ?? 'contacto@apumexico.com';
$locationLabel = $contactInfo['location_label'] ?? 'Ubicación';
$locationValue = $contactInfo['location_value'] ?? 'Ciudad de México, México';
$html = '<div class="contact-info">';
// Phone
$html .= '<div class="d-flex align-items-start mb-3">';
$html .= '<i class="bi bi-telephone-fill me-3 fs-5 contact-icon"></i>';
$html .= '<div>';
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($phoneLabel));
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($phoneValue));
$html .= '</div>';
$html .= '</div>';
// Email
$html .= '<div class="d-flex align-items-start mb-3">';
$html .= '<i class="bi bi-envelope-fill me-3 fs-5 contact-icon"></i>';
$html .= '<div>';
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($emailLabel));
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($emailValue));
$html .= '</div>';
$html .= '</div>';
// Location
$html .= '<div class="d-flex align-items-start">';
$html .= '<i class="bi bi-geo-alt-fill me-3 fs-5 contact-icon"></i>';
$html .= '<div>';
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($locationLabel));
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($locationValue));
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildJS(array $data): string
{
$messages = $data['messages'] ?? [];
$content = $data['content'] ?? [];
$successMessage = $messages['success_message'] ?? '¡Gracias por contactarnos! Te responderemos pronto.';
$errorMessage = $messages['error_message'] ?? 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.';
$sendingMessage = $messages['sending_message'] ?? 'Enviando...';
$submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
$submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
// AJAX URL for WordPress
$ajaxUrl = admin_url('admin-ajax.php');
$js = <<<JS
(function() {
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('roiContactForm');
if (!form) return;
form.addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = form.querySelector('button[type="submit"]');
const messageDiv = document.getElementById('roiContactFormMessage');
const originalBtnHtml = submitBtn.innerHTML;
const nonce = form.dataset.nonce;
// Disable button and show sending state
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>' + '{$sendingMessage}';
messageDiv.style.display = 'none';
// Collect form data
const formData = new FormData(form);
formData.append('action', 'roi_contact_form_submit');
formData.append('nonce', nonce);
formData.append('pageUrl', window.location.href);
formData.append('pageTitle', document.title);
try {
const response = await fetch('{$ajaxUrl}', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
messageDiv.className = 'col-12 mt-2 alert alert-success';
messageDiv.textContent = '{$successMessage}';
messageDiv.style.display = 'block';
form.reset();
} else {
messageDiv.className = 'col-12 mt-2 alert alert-danger';
messageDiv.textContent = result.data?.message || '{$errorMessage}';
messageDiv.style.display = 'block';
}
} catch (error) {
console.error('Contact form error:', error);
messageDiv.className = 'col-12 mt-2 alert alert-danger';
messageDiv.textContent = '{$errorMessage}';
messageDiv.style.display = 'block';
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnHtml;
}
});
});
})();
JS;
return $js;
}
}