added additional plugins

This commit is contained in:
Rodolfo Martinez 2025-12-12 19:05:48 -05:00
parent c85895d306
commit 00e60ec1b7
132 changed files with 27514 additions and 0 deletions

View file

@ -0,0 +1,481 @@
<?php
/**
* Admin-specific functionality.
*
* @package WPFMJ
* @subpackage WPFMJ/admin
*/
class WPFMJ_Admin {
/**
* The ID of this plugin.
*/
private $plugin_name;
/**
* The version of this plugin.
*/
private $version;
/**
* Initialize the class.
*/
public function __construct($plugin_name, $version) {
$this->plugin_name = $plugin_name;
$this->version = $version;
}
/**
* Register the stylesheets for the admin area.
*/
public function enqueue_styles() {
$screen = get_current_screen();
if ($screen && strpos($screen->id, 'wpfmj') !== false) {
wp_enqueue_style(
$this->plugin_name,
WPFMJ_PLUGIN_URL . 'admin/css/wpfmj-admin.css',
array(),
$this->version,
'all'
);
}
}
/**
* Register the JavaScript for the admin area.
*/
public function enqueue_scripts() {
$screen = get_current_screen();
if ($screen && strpos($screen->id, 'wpfmj') !== false) {
// Enqueue WordPress components
$asset_file = include(WPFMJ_PLUGIN_DIR . 'admin/js/wpfmj-wizard.asset.php');
if (!$asset_file) {
$asset_file = array(
'dependencies' => array(
'wp-element',
'wp-components',
'wp-i18n',
'wp-api-fetch',
),
'version' => $this->version
);
}
wp_enqueue_script(
$this->plugin_name,
WPFMJ_PLUGIN_URL . 'admin/js/wpfmj-wizard.js',
$asset_file['dependencies'],
$asset_file['version'],
true
);
wp_localize_script(
$this->plugin_name,
'wpfmjData',
array(
'restUrl' => rest_url(),
'nonce' => wp_create_nonce('wp_rest'),
'ajaxUrl' => admin_url('admin-ajax.php'),
'ajaxNonce' => wp_create_nonce('wpfmj_ajax'),
'editId' => isset($_GET['edit']) ? intval($_GET['edit']) : 0,
)
);
}
}
/**
* AJAX: Get all WPForms.
*/
public function ajax_get_forms() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$forms = wpforms()->form->get('', array('orderby' => 'title'));
$formatted_forms = array();
foreach ($forms as $form) {
$formatted_forms[] = array(
'id' => $form->ID,
'title' => $form->post_title,
);
}
wp_send_json_success($formatted_forms);
}
/**
* AJAX: Get form fields.
*/
public function ajax_get_form_fields() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$form_id = isset($_POST['form_id']) ? intval($_POST['form_id']) : 0;
if (!$form_id) {
wp_send_json_error('Invalid form ID');
}
$form = wpforms()->form->get($form_id);
if (!$form) {
wp_send_json_error('Form not found');
}
$form_data = wpforms_decode($form->post_content);
// Validate form_data structure
if (!is_array($form_data) || !isset($form_data['fields']) || !is_array($form_data['fields'])) {
wp_send_json_error('Invalid form data structure');
}
$fields = array();
foreach ($form_data['fields'] as $field) {
// Validate field structure
if (!is_array($field) || !isset($field['type']) || !isset($field['label'])) {
continue; // Skip invalid fields
}
$field_type = sanitize_text_field($field['type']);
$field_id = isset($field['id']) ? sanitize_text_field($field['id']) : '';
$field_label = sanitize_text_field($field['label']);
// Get choices for fields that have them
$choices = array();
if (in_array($field_type, array('checkbox', 'radio', 'select', 'payment-checkbox', 'payment-multiple', 'payment-select'))) {
if (isset($field['choices']) && is_array($field['choices'])) {
foreach ($field['choices'] as $choice) {
if (is_array($choice) && isset($choice['label'])) {
$choices[] = array(
'label' => sanitize_text_field($choice['label']),
'value' => isset($choice['value']) ? sanitize_text_field($choice['value']) : sanitize_text_field($choice['label']),
);
}
}
}
}
$fields[] = array(
'id' => $field_id,
'label' => $field_label,
'type' => $field_type,
'choices' => $choices,
);
}
wp_send_json_success($fields);
}
/**
* AJAX: Test Mailjet connection.
*/
public function ajax_test_mailjet() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
$api_secret = isset($_POST['api_secret']) ? sanitize_text_field($_POST['api_secret']) : '';
if (empty($api_key) || empty($api_secret)) {
wp_send_json_error('API credentials required');
}
$api = new WPFMJ_Mailjet_API($api_key, $api_secret);
$result = $api->test_connection();
if ($result) {
wp_send_json_success('Connection successful');
} else {
wp_send_json_error('Connection failed. Please check your credentials.');
}
}
/**
* AJAX: Get Mailjet lists.
*/
public function ajax_get_mailjet_lists() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
$api_secret = isset($_POST['api_secret']) ? sanitize_text_field($_POST['api_secret']) : '';
if (empty($api_key) || empty($api_secret)) {
wp_send_json_error('API credentials required');
}
$api = new WPFMJ_Mailjet_API($api_key, $api_secret);
$lists = $api->get_lists();
if (is_wp_error($lists)) {
wp_send_json_error($lists->get_error_message());
}
$formatted_lists = array();
foreach ($lists as $list) {
$formatted_lists[] = array(
'id' => $list['ID'],
'name' => $list['Name'],
);
}
wp_send_json_success($formatted_lists);
}
/**
* AJAX: Save automation.
*/
public function ajax_save_automation() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$data = isset($_POST['data']) ? $_POST['data'] : array();
$edit_id = isset($_POST['edit_id']) ? intval($_POST['edit_id']) : 0;
// Validate required fields
$required = array('title', 'form_id', 'field_mapping', 'trigger_field_id', 'api_key', 'api_secret', 'list_mappings', 'activate');
foreach ($required as $field) {
if (!isset($data[$field]) || (is_string($data[$field]) && empty($data[$field]))) {
wp_send_json_error("Missing required field: {$field}");
}
}
// Sanitize and validate field_mapping
if (!is_array($data['field_mapping']) || !isset($data['field_mapping']['email'])) {
wp_send_json_error('Invalid field mapping structure');
}
$field_mapping = array(
'email' => sanitize_text_field($data['field_mapping']['email']),
'firstname' => isset($data['field_mapping']['firstname']) ? sanitize_text_field($data['field_mapping']['firstname']) : '',
'lastname' => isset($data['field_mapping']['lastname']) ? sanitize_text_field($data['field_mapping']['lastname']) : '',
);
// Sanitize and validate list_mappings
if (!is_array($data['list_mappings'])) {
wp_send_json_error('Invalid list mappings structure');
}
$list_mappings = array();
foreach ($data['list_mappings'] as $key => $value) {
$list_mappings[sanitize_text_field($key)] = sanitize_text_field($value);
}
// Sanitize API credentials
$api_key = sanitize_text_field($data['api_key']);
$api_secret = sanitize_text_field($data['api_secret']);
// Encrypt API credentials
$encrypted_key = WPFMJ_Encryption::encrypt($api_key);
$encrypted_secret = WPFMJ_Encryption::encrypt($api_secret);
if (empty($encrypted_key) || empty($encrypted_secret)) {
wp_send_json_error('Failed to encrypt API credentials');
}
// Prepare config
$config = array(
'form_id' => intval($data['form_id']),
'field_mapping' => $field_mapping,
'trigger_field_id' => sanitize_text_field($data['trigger_field_id']),
'api_key' => $encrypted_key,
'api_secret' => $encrypted_secret,
'list_mappings' => $list_mappings,
);
// Create or update post
$post_data = array(
'post_title' => sanitize_text_field($data['title']),
'post_type' => 'wpfmj_automation',
'post_status' => $data['activate'] ? 'publish' : 'draft',
);
if ($edit_id) {
$post_data['ID'] = $edit_id;
$post_id = wp_update_post($post_data);
} else {
$post_id = wp_insert_post($post_data);
}
if (is_wp_error($post_id)) {
wp_send_json_error($post_id->get_error_message());
}
// Save config
update_post_meta($post_id, '_wpfmj_config', $config);
update_post_meta($post_id, '_wpfmj_form_id', $config['form_id']);
wp_send_json_success(array(
'id' => $post_id,
'message' => 'Automation saved successfully',
));
}
/**
* AJAX: Get automation.
*/
public function ajax_get_automation() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if (!$id) {
wp_send_json_error('Invalid ID');
}
$post = get_post($id);
$config = get_post_meta($id, '_wpfmj_config', true);
if (!$post || !$config) {
wp_send_json_error('Automation not found');
}
// Decrypt API credentials
$decrypted_key = WPFMJ_Encryption::decrypt($config['api_key']);
$decrypted_secret = WPFMJ_Encryption::decrypt($config['api_secret']);
// Check for decryption failures
if ($decrypted_key === false || $decrypted_secret === false) {
wp_send_json_error('Failed to decrypt API credentials. The encryption key may have changed.');
}
$config['api_key'] = $decrypted_key;
$config['api_secret'] = $decrypted_secret;
wp_send_json_success(array(
'title' => $post->post_title,
'status' => $post->post_status,
'config' => $config,
));
}
/**
* AJAX: Toggle automation status.
*/
public function ajax_toggle_automation() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if (!$id) {
wp_send_json_error('Invalid ID');
}
$post = get_post($id);
if (!$post) {
wp_send_json_error('Automation not found');
}
$new_status = $post->post_status === 'publish' ? 'draft' : 'publish';
wp_update_post(array(
'ID' => $id,
'post_status' => $new_status,
));
wp_send_json_success(array(
'status' => $new_status,
'message' => 'Automation ' . ($new_status === 'publish' ? 'activated' : 'paused'),
));
}
/**
* AJAX: Delete automation.
*/
public function ajax_delete_automation() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if (!$id) {
wp_send_json_error('Invalid ID');
}
$result = wp_delete_post($id, true);
if ($result) {
wp_send_json_success('Automation deleted');
} else {
wp_send_json_error('Failed to delete automation');
}
}
/**
* AJAX: Get dashboard data.
*/
public function ajax_get_dashboard_data() {
check_ajax_referer('wpfmj_ajax', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permission denied');
}
$automations = get_posts(array(
'post_type' => 'wpfmj_automation',
'post_status' => array('publish', 'draft'),
'posts_per_page' => -1,
));
if (empty($automations)) {
wp_send_json_success(array());
return;
}
// Pre-load all post meta to prevent N+1 queries
$automation_ids = wp_list_pluck($automations, 'ID');
update_post_meta_cache($automation_ids);
// Pre-load all error counts in a single query to prevent N+1
$logger = new WPFMJ_Error_Logger();
$error_counts = $logger->get_error_counts_bulk($automation_ids);
$data = array();
foreach ($automations as $automation) {
$config = get_post_meta($automation->ID, '_wpfmj_config', true);
$form = wpforms()->form->get($config['form_id']);
$data[] = array(
'id' => $automation->ID,
'title' => $automation->post_title,
'form_name' => $form ? $form->post_title : 'Unknown Form',
'status' => $automation->post_status,
'error_count' => isset($error_counts[$automation->ID]) ? $error_counts[$automation->ID] : 0,
'created' => $automation->post_date,
);
}
wp_send_json_success($data);
}
}

View file

@ -0,0 +1,241 @@
<?php
/**
* Dashboard page functionality.
*
* @package WPFMJ
* @subpackage WPFMJ/admin
*/
class WPFMJ_Dashboard {
/**
* Add menu page.
*/
public function add_menu_page() {
add_menu_page(
__('Mailjet Automations', 'wpforms-mailjet-automation'),
__('Mailjet Automations', 'wpforms-mailjet-automation'),
'manage_options',
'wpfmj-dashboard',
array($this, 'render_dashboard'),
'dashicons-email-alt',
30
);
add_submenu_page(
'wpfmj-dashboard',
__('All Automations', 'wpforms-mailjet-automation'),
__('All Automations', 'wpforms-mailjet-automation'),
'manage_options',
'wpfmj-dashboard',
array($this, 'render_dashboard')
);
add_submenu_page(
'wpfmj-dashboard',
__('Add New Automation', 'wpforms-mailjet-automation'),
__('Add New', 'wpforms-mailjet-automation'),
'manage_options',
'wpfmj-add-new',
array($this, 'render_wizard')
);
}
/**
* Render dashboard page.
*/
public function render_dashboard() {
// Verify user has proper capabilities
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'wpforms-mailjet-automation'));
}
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Mailjet Automations', 'wpforms-mailjet-automation'); ?></h1>
<a href="<?php echo esc_url(admin_url('admin.php?page=wpfmj-add-new')); ?>" class="page-title-action">
<?php esc_html_e('Add New', 'wpforms-mailjet-automation'); ?>
</a>
<hr class="wp-header-end">
<div id="wpfmj-dashboard-root"></div>
<div id="wpfmj-dashboard-loading" style="padding: 40px; text-align: center;">
<span class="spinner is-active" style="float: none; margin: 0 auto;"></span>
<p><?php esc_html_e('Loading automations...', 'wpforms-mailjet-automation'); ?></p>
</div>
<script type="text/javascript">
// Render dashboard table
jQuery(document).ready(function($) {
function loadDashboard() {
$.ajax({
url: wpfmjData.ajaxUrl,
type: 'POST',
data: {
action: 'wpfmj_get_dashboard_data',
nonce: wpfmjData.ajaxNonce
},
success: function(response) {
$('#wpfmj-dashboard-loading').hide();
if (response.success) {
renderTable(response.data);
} else {
var errorMessage = $('<div>').text(response.data).html();
$('#wpfmj-dashboard-root').html('<div class="notice notice-error"><p>' + errorMessage + '</p></div>');
}
},
error: function() {
$('#wpfmj-dashboard-loading').hide();
$('#wpfmj-dashboard-root').html('<div class="notice notice-error"><p>Failed to load automations.</p></div>');
}
});
}
function renderTable(automations) {
if (automations.length === 0) {
$('#wpfmj-dashboard-root').html(
'<div class="wpfmj-empty-state" style="text-align: center; padding: 60px 20px;">' +
'<p style="font-size: 18px; color: #666; margin-bottom: 20px;">No automations yet. Create your first automation to get started!</p>' +
'<a href="' + wpfmjData.ajaxUrl.replace('admin-ajax.php', 'admin.php?page=wpfmj-add-new') + '" class="button button-primary button-large">Create Automation</a>' +
'</div>'
);
return;
}
var html = '<table class="wp-list-table widefat fixed striped">';
html += '<thead><tr>';
html += '<th>Title</th>';
html += '<th>Form</th>';
html += '<th>Status</th>';
html += '<th>Errors</th>';
html += '<th>Created</th>';
html += '<th>Actions</th>';
html += '</tr></thead><tbody>';
automations.forEach(function(automation) {
var statusClass = automation.status === 'publish' ? 'active' : 'inactive';
var statusText = automation.status === 'publish' ? 'Active' : 'Paused';
var errorBadge = automation.error_count > 0 ? '<span class="wpfmj-error-badge">' + parseInt(automation.error_count) + '</span>' : '—';
// Escape data for safe HTML output
var title = $('<div>').text(automation.title).html();
var formName = $('<div>').text(automation.form_name).html();
var automationId = parseInt(automation.id);
html += '<tr>';
html += '<td><strong>' + title + '</strong></td>';
html += '<td>' + formName + '</td>';
html += '<td><span class="wpfmj-status-badge wpfmj-status-' + statusClass + '">' + statusText + '</span></td>';
html += '<td>' + errorBadge + '</td>';
html += '<td>' + new Date(automation.created).toLocaleDateString() + '</td>';
html += '<td>';
html += '<a href="admin.php?page=wpfmj-add-new&edit=' + automationId + '" class="button button-small">Edit</a> ';
html += '<button class="button button-small wpfmj-toggle-btn" data-id="' + automationId + '" data-status="' + automation.status + '">' + (automation.status === 'publish' ? 'Pause' : 'Activate') + '</button> ';
html += '<button class="button button-small button-link-delete wpfmj-delete-btn" data-id="' + automationId + '">Delete</button>';
html += '</td>';
html += '</tr>';
});
html += '</tbody></table>';
$('#wpfmj-dashboard-root').html(html);
}
// Use event delegation to prevent memory leaks
// Unbind previous handlers before binding new ones
$('#wpfmj-dashboard-root').off('click', '.wpfmj-toggle-btn').on('click', '.wpfmj-toggle-btn', function() {
var btn = $(this);
var id = btn.data('id');
toggleAutomation(id, btn);
});
$('#wpfmj-dashboard-root').off('click', '.wpfmj-delete-btn').on('click', '.wpfmj-delete-btn', function() {
var id = $(this).data('id');
if (confirm('Are you sure you want to delete this automation? This cannot be undone.')) {
deleteAutomation(id);
}
});
function toggleAutomation(id, btn) {
btn.prop('disabled', true);
$.ajax({
url: wpfmjData.ajaxUrl,
type: 'POST',
data: {
action: 'wpfmj_toggle_automation',
nonce: wpfmjData.ajaxNonce,
id: id
},
success: function(response) {
if (response.success) {
loadDashboard();
} else {
var errorMessage = $('<div>').text(response.data).html();
alert('Error: ' + errorMessage);
btn.prop('disabled', false);
}
},
error: function() {
alert('Failed to toggle automation');
btn.prop('disabled', false);
}
});
}
function deleteAutomation(id) {
$.ajax({
url: wpfmjData.ajaxUrl,
type: 'POST',
data: {
action: 'wpfmj_delete_automation',
nonce: wpfmjData.ajaxNonce,
id: id
},
success: function(response) {
if (response.success) {
loadDashboard();
} else {
var errorMessage = $('<div>').text(response.data).html();
alert('Error: ' + errorMessage);
}
},
error: function() {
alert('Failed to delete automation');
}
});
}
loadDashboard();
});
</script>
</div>
<?php
}
/**
* Render wizard page.
*/
public function render_wizard() {
// Verify user has proper capabilities
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'wpforms-mailjet-automation'));
}
$edit_id = isset($_GET['edit']) ? intval($_GET['edit']) : 0;
// If editing, verify the post exists and user can edit it
if ($edit_id) {
$post = get_post($edit_id);
if (!$post || $post->post_type !== 'wpfmj_automation') {
wp_die(__('Invalid automation ID.', 'wpforms-mailjet-automation'));
}
}
$page_title = $edit_id ? __('Edit Automation', 'wpforms-mailjet-automation') : __('Add New Automation', 'wpforms-mailjet-automation');
?>
<div class="wrap">
<h1><?php echo esc_html($page_title); ?></h1>
<div id="wpfmj-wizard-root"></div>
</div>
<?php
}
}

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.

View file

@ -0,0 +1,296 @@
/* Dashboard Styles */
.wpfmj-status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.wpfmj-status-active {
background-color: #d4edda;
color: #155724;
}
.wpfmj-status-inactive {
background-color: #f8d7da;
color: #721c24;
}
.wpfmj-error-badge {
display: inline-block;
background-color: #dc3545;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: bold;
}
.wpfmj-empty-state {
background: #f9f9f9;
border: 2px dashed #ddd;
border-radius: 8px;
}
/* Wizard Styles */
.wpfmj-wizard-container {
max-width: 900px;
margin: 20px 0;
background: white;
border: 1px solid #ccd0d4;
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.wpfmj-wizard-header {
padding: 20px 30px;
border-bottom: 1px solid #e0e0e0;
}
.wpfmj-wizard-progress {
display: flex;
justify-content: space-between;
margin: 0;
padding: 0;
list-style: none;
}
.wpfmj-wizard-progress li {
flex: 1;
text-align: center;
position: relative;
font-size: 13px;
color: #999;
}
.wpfmj-wizard-progress li:not(:last-child)::after {
content: '';
position: absolute;
top: 12px;
right: -50%;
width: 100%;
height: 2px;
background: #e0e0e0;
z-index: -1;
}
.wpfmj-wizard-progress li.active {
color: #2271b1;
font-weight: 600;
}
.wpfmj-wizard-progress li.completed {
color: #00a32a;
}
.wpfmj-wizard-progress li.active::before,
.wpfmj-wizard-progress li.completed::before {
content: '';
display: block;
width: 24px;
height: 24px;
margin: 0 auto 8px;
border-radius: 50%;
background: #2271b1;
}
.wpfmj-wizard-progress li.completed::before {
background: #00a32a;
}
.wpfmj-wizard-body {
padding: 30px;
min-height: 400px;
}
.wpfmj-wizard-footer {
padding: 20px 30px;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
}
/* Step Styles */
.wpfmj-step {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.wpfmj-step h2 {
margin: 0 0 20px;
font-size: 22px;
font-weight: 600;
}
.wpfmj-step p.description {
color: #666;
margin-bottom: 30px;
}
.wpfmj-field-group {
margin-bottom: 25px;
}
.wpfmj-field-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
font-size: 14px;
}
.wpfmj-field-group input[type="text"],
.wpfmj-field-group input[type="password"],
.wpfmj-field-group select {
width: 100%;
max-width: 500px;
}
/* Two Column Layout */
.wpfmj-two-column {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 20px;
}
.wpfmj-column h3 {
margin: 0 0 15px;
font-size: 16px;
font-weight: 600;
padding-bottom: 10px;
border-bottom: 2px solid #2271b1;
}
.wpfmj-mapping-row {
display: flex;
align-items: center;
gap: 15px;
padding: 12px;
background: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-bottom: 10px;
}
.wpfmj-mapping-row .wpfmj-answer {
flex: 1;
font-weight: 500;
}
.wpfmj-mapping-row select {
flex: 1;
min-width: 200px;
}
.wpfmj-mapping-row button {
flex-shrink: 0;
}
/* Review Step */
.wpfmj-review-section {
background: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 20px;
margin-bottom: 20px;
}
.wpfmj-review-section h3 {
margin: 0 0 15px;
font-size: 16px;
font-weight: 600;
color: #2271b1;
}
.wpfmj-review-item {
display: flex;
padding: 8px 0;
border-bottom: 1px solid #e0e0e0;
}
.wpfmj-review-item:last-child {
border-bottom: none;
}
.wpfmj-review-label {
flex: 0 0 180px;
font-weight: 600;
color: #555;
}
.wpfmj-review-value {
flex: 1;
color: #333;
}
/* Alerts */
.wpfmj-alert {
padding: 12px 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.wpfmj-alert-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.wpfmj-alert-error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.wpfmj-alert-info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
/* Loading States */
.wpfmj-loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #2271b1;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 782px) {
.wpfmj-two-column {
grid-template-columns: 1fr;
}
.wpfmj-wizard-footer {
flex-direction: column;
gap: 10px;
}
.wpfmj-mapping-row {
flex-direction: column;
align-items: stretch;
}
}

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.

View file

@ -0,0 +1,20 @@
<?php
/**
* Asset file for the wizard JavaScript bundle.
* This file would normally be generated by @wordpress/scripts build process.
*
* To build the React app:
* 1. Install dependencies: npm install @wordpress/scripts --save-dev
* 2. Add to package.json scripts: "build": "wp-scripts build assets/src/wizard/App.jsx --output-path=admin/js/wpfmj-wizard.js"
* 3. Run: npm run build
*/
return array(
'dependencies' => array(
'wp-element',
'wp-components',
'wp-i18n',
'wp-api-fetch',
),
'version' => '1.0.0'
);