PluginProbe
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets / 4.5.6
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets v4.5.6
4.5.6 4.5.5 4.5.4 4.2.1 4.2.2 4.2.3 4.5.0 4.5.2 4.5.3 4.2.0 4.1.18 4.1.17 4.1.16 4.1.15 4.1.14 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.0.9 4.1.0 4.1.1 4.1.2 All 148 releases
← All changes | includes/setup-wizard/views/integration.php +461 -197 4.0.9 → 4.5.6 View file →
@@ -8,105 +8,61 @@
8 8 if (!defined('ABSPATH')) {
9 9 exit;
10 10 }
11 11
12 -// Include the plugin API fetcher and helper
13 -require_once __DIR__ . '/../class-plugin-api-fetcher.php';
12 +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template partial included within a method; variables are method-scoped, not global.
13 +
14 +// Include the required classes
14 15 require_once __DIR__ . '/../class-plugin-integration-helper.php';
16 +require_once __DIR__ . '/../class-remote-data-handler.php';
15 17
16 -// Function to format date in human-readable format
17 -function format_last_updated($date_string) {
18 - if (empty($date_string)) {
19 - return '';
18 +// Helper function for time formatting
19 +if (!function_exists('format_last_updated_usk')) {
20 + function format_last_updated_usk($date_string) {
21 + if (empty($date_string)) {
22 + return __('Unknown', 'ultimate-post-kit');
23 + }
24 +
25 + $date = strtotime($date_string);
26 + if (!$date) {
27 + return __('Unknown', 'ultimate-post-kit');
28 + }
29 +
30 + $diff = current_time('timestamp') - $date;
31 +
32 + if ($diff < 60) {
33 + return __('Just now', 'ultimate-post-kit');
34 + } elseif ($diff < 3600) {
35 + $minutes = floor($diff / 60);
36 + /* translators: %d: number of minutes */
37 + return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'ultimate-post-kit'), $minutes);
38 + } elseif ($diff < 86400) {
39 + $hours = floor($diff / 3600);
40 + /* translators: %d: number of hours */
41 + return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'ultimate-post-kit'), $hours);
42 + } elseif ($diff < 2592000) { // 30 days
43 + $days = floor($diff / 86400);
44 + /* translators: %d: number of days */
45 + return sprintf(_n('%d day ago', '%d days ago', $days, 'ultimate-post-kit'), $days);
46 + } elseif ($diff < 31536000) { // 1 year
47 + $months = floor($diff / 2592000);
48 + /* translators: %d: number of months */
49 + return sprintf(_n('%d month ago', '%d months ago', $months, 'ultimate-post-kit'), $months);
50 + } else {
51 + $years = floor($diff / 31536000);
52 + /* translators: %d: number of years */
53 + return sprintf(_n('%d year ago', '%d years ago', $years, 'ultimate-post-kit'), $years);
54 + }
20 55 }
21 -
22 - // Try to parse the date
23 - $date = strtotime($date_string);
24 - if ($date === false) {
25 - return $date_string; // Return original if parsing fails
26 - }
27 -
28 - // Calculate time difference
29 - $now = time();
30 - $diff = $now - $date;
31 -
32 - // Convert to human-readable format
33 - if ($diff < 60) {
34 - return __('just now', 'ultimate-post-kit');
35 - } elseif ($diff < 3600) {
36 - $minutes = floor($diff / 60);
37 - return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'ultimate-post-kit'), $minutes);
38 - } elseif ($diff < 86400) {
39 - $hours = floor($diff / 3600);
40 - return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'ultimate-post-kit'), $hours);
41 - } elseif ($diff < 2592000) { // 30 days
42 - $days = floor($diff / 86400);
43 - return sprintf(_n('%d day ago', '%d days ago', $days, 'ultimate-post-kit'), $days);
44 - } elseif ($diff < 31536000) { // 1 year
45 - $months = floor($diff / 2592000);
46 - return sprintf(_n('%d month ago', '%d months ago', $months, 'ultimate-post-kit'), $months);
47 - } else {
48 - $years = floor($diff / 31536000);
49 - return sprintf(_n('%d year ago', '%d years ago', $years, 'ultimate-post-kit'), $years);
50 - }
51 56 }
52 57
53 -// Function to generate fallback URLs for plugin logos
54 -function get_plugin_fallback_urls($plugin_slug) {
55 - // Handle different plugin slug formats
56 - if (strpos($plugin_slug, '/') !== false) {
57 - // If it's a file path like 'plugin-name/plugin-name.php', extract directory
58 - $plugin_slug_clean = dirname($plugin_slug);
59 - } else {
60 - // If it's just the plugin directory name, use it directly
61 - $plugin_slug_clean = $plugin_slug;
62 - }
63 -
64 - // Custom icon URLs for specific plugins that might not be on WordPress.org
65 - $custom_icons = [
66 - 'bdthemes-element-pack-lite' => [
67 - 'https://ps.w.org/bdthemes-element-pack-lite/assets/icon-256x256.png',
68 - 'https://ps.w.org/bdthemes-element-pack-lite/assets/icon-128x128.png',
69 - ],
70 - 'live-copy-paste' => [
71 - 'https://ps.w.org/live-copy-paste/assets/icon-256x256.png',
72 - 'https://ps.w.org/live-copy-paste/assets/icon-128x128.png',
73 - ],
74 - 'spin-wheel' => [
75 - 'https://ps.w.org/spin-wheel/assets/icon-256x256.png',
76 - 'https://ps.w.org/spin-wheel/assets/icon-128x128.png',
77 - ],
78 - 'ai-image' => [
79 - 'https://ps.w.org/ai-image/assets/icon-256x256.png',
80 - 'https://ps.w.org/ai-image/assets/icon-128x128.png',
81 - ],
82 - 'smart-admin-assistant' => [
83 - 'https://ps.w.org/smart-admin-assistant/assets/icon-256x256.png',
84 - 'https://ps.w.org/smart-admin-assistant/assets/icon-128x128.png',
85 - ],
86 - 'website-accessibility' => [
87 - 'https://ps.w.org/website-accessibility/assets/icon-256x256.png',
88 - 'https://ps.w.org/website-accessibility/assets/icon-128x128.png',
89 - ],
90 - ];
91 -
92 - // Return custom icons if available, otherwise use default WordPress.org URLs
93 - if (isset($custom_icons[$plugin_slug_clean])) {
94 - return $custom_icons[$plugin_slug_clean];
95 - }
96 -
97 - return [
98 - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-256x256.gif", // Try GIF first
99 - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-256x256.png", // Then PNG
100 - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-128x128.gif", // Medium GIF
101 - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-128x128.png", // Medium PNG
102 - ];
103 -}
58 +// Helper function for fallback URLs
104 59
105 -// Define plugin slugs to fetch data for
60 +// Define plugin slugs
106 61 $plugin_slugs = array(
107 62 'bdthemes-element-pack-lite',
108 - 'bdthemes-prime-slider-lite',
63 + 'bdthemes-prime-slider-lite/bdthemes-prime-slider.php',
64 + 'ultimate-post-kit',
109 65 'ultimate-store-kit',
110 66 'zoloblocks',
111 67 'pixel-gallery',
112 68 'live-copy-paste',
@@ -115,14 +71,20 @@
115 71 'dark-reader',
116 72 'ar-viewer',
117 73 'smart-admin-assistant',
118 74 'website-accessibility',
75 +);
119 76
120 - // Add more plugin slugs here as needed
121 -);
77 +// Get enhanced plugin data using the remote data handler
78 +$upk_plugins = Remote_Data_Handler::get_remote_plugins();
122 79
123 -// Get plugin data using the helper
124 -$upk_plugins = Plugin_Integration_Helper::build_plugin_data($plugin_slugs);
80 +// Check if we have cached data
81 +$has_cached_data = !empty($upk_plugins);
82 +
83 +// If no cached data, don't fetch immediately - let JavaScript handle it
84 +if (!$has_cached_data) {
85 + $upk_plugins = []; // Empty array for initial load
86 +}
125 87 ?>
126 88
127 89 <div class="bdt-wizard-step bdt-setup-wizard-integration" data-step="integration">
128 90 <h2><?php esc_html_e('Add More Firepower', 'ultimate-post-kit'); ?></h2>
@@ -132,124 +94,159 @@
132 94 <div id="plugin-install-progress" class="progress-bar"></div>
133 95 </div>
134 96
135 97 <form method="POST" id="upk-install-plugins">
136 - <div class="bdt-plugin-list">
137 - <?php
138 - foreach ($upk_plugins as $plugin) :
139 - $is_active = is_plugin_active($plugin['slug']);
140 - $is_recommended = $plugin['recommended'] && !$is_active;
141 - ?>
142 - <label class="plugin-item" data-slug="<?php echo esc_attr($plugin['slug']); ?>">
143 - <span class="bdt-flex bdt-flex-middle bdt-flex-between bdt-margin-small-bottom">
144 - <span class="bdt-plugin-logo">
145 - <?php
146 - $logo_url = $plugin['logo'] ?? '';
147 - $plugin_name = $plugin['name'] ?? '';
148 - $plugin_slug = $plugin['slug'] ?? '';
98 + <!-- Loading state - shown during plugin installation -->
99 + <div class="upk-loading-state" id="upk-install-loading" style="display: none; text-align: center; padding: 40px;">
100 + <div class="upk-loading-dots">
101 + <div class="upk-loading-dot"></div>
102 + <div class="upk-loading-dot"></div>
103 + <div class="upk-loading-dot"></div>
104 + </div>
105 + <p style="margin-top: 20px;" id="upk-loading-message"><?php esc_html_e('Installing plugins...', 'ultimate-post-kit'); ?></p>
106 + </div>
107 +
108 + <!-- Initial loading state - shown while fetching plugin data -->
109 + <?php if (!$has_cached_data): ?>
110 + <div class="upk-loading-state" id="upk-initial-loading" style="text-align: center; padding: 40px;">
111 + <div class="upk-loading-dots">
112 + <div class="upk-loading-dot"></div>
113 + <div class="upk-loading-dot"></div>
114 + <div class="upk-loading-dot"></div>
115 + </div>
116 + <p style="margin-top: 20px;"><?php esc_html_e('Loading plugin data...', 'ultimate-post-kit'); ?></p>
117 + </div>
118 + <?php endif; ?>
119 +
120 + <div class="bdt-plugin-list" id="upk-integration-plugin-list">
121 + <?php if ($has_cached_data): ?>
122 + <?php
123 + $predefined = \UltimatePostKit\SetupWizard\Plugin_Integration_Helper::get_predefined_plugins();
124 + $recommended_by_slug = [];
125 + foreach ($predefined as $key => $config) {
126 + $dir = (strpos($key, '/') !== false) ? dirname($key) : $key;
127 + $recommended_by_slug[ $dir ] = !empty($config['recommended']);
128 + }
129 + foreach ($upk_plugins as $slug_key => $plugin) :
130 + // Skip own plugin (Ultimate Post Kit) when printing only; data still includes it for other plugins
131 + if ($slug_key === 'ultimate-post-kit' ) continue;
132 + // Use enhanced status if available, otherwise fall back to old method
133 + $plugin_status = $plugin['status'] ?? 'unknown';
134 + if ($plugin_status === 'unknown') {
135 + // Fallback to old method for compatibility
136 + $is_active = is_plugin_active($plugin['slug']);
137 + } else {
138 + // Use enhanced status
139 + $is_active = ($plugin_status === 'active');
140 + }
141 + $plugin_recommended = !empty($recommended_by_slug[ $slug_key ]);
142 + $is_recommended = $plugin_recommended && !$is_active;
143 + ?>
144 + <label class="plugin-item" data-slug="<?php echo esc_attr($plugin['slug']); ?>">
145 + <span class="bdt-flex bdt-flex-middle bdt-flex-between bdt-margin-small-bottom">
146 + <span class="bdt-plugin-logo">
147 + <?php
148 + $logo_url = $plugin['logo'] ?? '';
149 + $plugin_name = $plugin['name'] ?? '';
150 + $plugin_slug = $plugin['slug'] ?? '';
151 +
152 + if (!empty($logo_url) && filter_var($logo_url, FILTER_VALIDATE_URL)) {
153 + // Show the original logo from API
154 + echo '<img src="' . esc_url($logo_url) . '" alt="' . esc_attr($plugin_name) . '" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
155 + echo '<div class="default-plugin-icon" style="display:none;">📦</div>';
156 + } else {
157 + // Generate fallback URLs for WordPress.org
158 + // No icon in the API response, show the local placeholder.
159 + echo '<div class="default-plugin-icon" style="display:flex;">📦</div>';
160 + }
161 + ?>
162 + </span>
149 163
150 - if (!empty($logo_url) && filter_var($logo_url, FILTER_VALIDATE_URL)) {
151 - // Show the original logo from API
152 - echo '<img src="' . esc_url($logo_url) . '" alt="' . esc_attr($plugin_name) . '" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
153 - echo '<div class="default-plugin-icon" style="display:none;">📦</div>';
164 + <div class="bdt-plugin-badge-switch-wrap">
165 +
166 + <?php if ($is_recommended) : ?>
167 + <span class="recommended-badge"><?php esc_html_e('Recommended', 'ultimate-post-kit'); ?></span>
168 + <?php endif; ?>
169 +
170 + <?php if ($is_active) : ?>
171 + <span class="active-badge"><?php esc_html_e('ACTIVE', 'ultimate-post-kit'); ?></span>
172 + <?php endif; ?>
173 + <?php
174 + if (!$is_active) : ?>
175 + <label class="switch">
176 + <input type="checkbox" class="plugin-slider-checkbox" <?php echo $plugin_recommended ? 'checked' : ''; ?>
177 + name="plugins[]<?php echo isset($plugin['slug']) ? esc_attr($plugin['slug']) : ''; ?>">
178 + <span class="slider round"></span>
179 + </label>
180 + <?php
181 + endif;
182 + ?>
183 + </div>
184 + </span>
185 + <div class="bdt-flex bdt-flex-middle">
186 + <span class="bdt-plugin-name">
187 + <?php echo esc_html($plugin['name']); ?>
188 + </span>
189 + </div>
190 +
191 + <span class="active-installs">
192 + <?php esc_html_e('Active Installs: ', 'ultimate-post-kit');
193 + if (isset($plugin['active_installs_count']) && $plugin['active_installs_count'] > 0) {
194 + echo ' <span class="installs-count">' . number_format($plugin['active_installs_count']) . '+' . '</span>';
154 195 } else {
155 - // Generate fallback URLs for WordPress.org
156 - // Extract the actual plugin slug from the file path format
157 - $actual_slug = str_replace('.php', '', basename($plugin_slug));
158 - $fallback_urls = get_plugin_fallback_urls($actual_slug);
159 -
160 - echo '<img src="' . esc_url($fallback_urls[0]) . '" alt="' . esc_attr($plugin_name) . '" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">';
161 - echo '<div class="default-plugin-icon" style="display:none;">📦</div>';
196 + echo '<span class="installs-count">Fewer than 10</span>';
162 197 }
163 198 ?>
164 199 </span>
165 -
166 - <div class="bdt-plugin-badge-switch-wrap">
167 200
168 - <?php if ($is_recommended) : ?>
169 - <span class="recommended-badge"><?php esc_html_e('Recommended', 'ultimate-post-kit'); ?></span>
201 + <?php if (isset($plugin['downloaded_formatted']) && !empty($plugin['downloaded_formatted'])): ?>
202 + <span class="downloads"><?php esc_html_e('Downloads: ', 'ultimate-post-kit'); echo esc_html($plugin['downloaded_formatted']); ?></span>
170 203 <?php endif; ?>
171 204
172 - <?php if ($is_active) : ?>
173 - <span class="active-badge"><?php esc_html_e('ACTIVE', 'ultimate-post-kit'); ?></span>
174 - <?php endif; ?>
175 - <?php
176 - if (!$is_active) : ?>
177 - <label class="switch">
178 - <input type="checkbox" class="plugin-slider-checkbox" <?php echo wp_kses_post($plugin['recommended']) ? 'checked' : ''; ?>
179 - name="plugins[]<?php echo isset($plugin['slug']) ? wp_kses_post($plugin['slug']) : ''; ?>">
180 - <span class="slider round"></span>
181 - </label>
182 - <?php
183 - endif;
184 - ?>
185 - </div>
186 - </span>
187 - <div class="bdt-flex bdt-flex-middle">
188 - <span class="bdt-plugin-name">
189 - <?php echo wp_kses_post($plugin['name']); ?>
205 + <div class="rating-section">
206 + <div class="wporg-ratings" title="<?php echo esc_attr($plugin['rating'] ?? '0'); ?> out of 5 stars" style="color:var(--wp--preset--color--pomegrade-1, #e26f56);">
207 + <?php
208 + $rating = floatval($plugin['rating'] ?? 0);
209 + $full_stars = floor($rating);
210 + $has_half_star = ($rating - $full_stars) >= 0.5;
211 + $empty_stars = 5 - $full_stars - ($has_half_star ? 1 : 0);
212 +
213 + // Full stars
214 + for ($i = 0; $i < $full_stars; $i++) {
215 + echo '<span class="dashicons dashicons-star-filled"></span>';
216 + }
217 +
218 + // Half star
219 + if ($has_half_star) {
220 + echo '<span class="dashicons dashicons-star-half"></span>';
221 + }
222 +
223 + // Empty stars
224 + for ($i = 0; $i < $empty_stars; $i++) {
225 + echo '<span class="dashicons dashicons-star-empty"></span>';
226 + }
227 + ?>
228 + </div>
229 + <span class="rating-text">
230 + <?php echo esc_html($plugin['rating'] ?? '0'); ?> out of 5 stars.
231 + <?php if (isset($plugin['num_ratings']) && $plugin['num_ratings'] > 0): ?>
232 + <span class="rating-count">(<?php echo number_format($plugin['num_ratings']); ?> ratings)</span>
233 + <?php endif; ?>
190 234 </span>
191 235 </div>
192 236
193 - <!-- <span class="plugin-text">
194 - <?php //echo wp_kses_post($plugin['description']); ?>
195 - </span> -->
196 - <span class="active-installs">
197 - <?php esc_html_e('Active Installs: ', 'ultimate-post-kit');
198 - // echo wp_kses_post($plugin['active_installs'] ?? '0');
199 - if (isset($plugin['active_installs_count']) && $plugin['active_installs_count'] > 0) {
200 - echo ' <span class="installs-count">' . number_format($plugin['active_installs_count']) . '+' . '</span>';
201 - }
202 - ?>
203 - </span>
237 + <?php
238 + // Use the enhanced last_updated_formatted if available, otherwise fall back to formatting
239 + if (isset($plugin['last_updated_formatted']) && !empty($plugin['last_updated_formatted'])): ?>
240 + <span class="last-updated"><?php esc_html_e('Last Updated: ', 'ultimate-post-kit'); echo esc_html($plugin['last_updated_formatted']); ?></span>
241 + <?php elseif (isset($plugin['last_updated']) && !empty($plugin['last_updated'])): ?>
242 + <span class="last-updated"><?php esc_html_e('Last Updated: ', 'ultimate-post-kit'); echo esc_html(format_last_updated_usk($plugin['last_updated'])); ?></span>
243 + <?php endif; ?>
204 244
205 - <?php if (isset($plugin['downloaded_formatted']) && !empty($plugin['downloaded_formatted'])): ?>
206 - <span class="downloads"><?php esc_html_e('Downloads: ', 'ultimate-post-kit'); echo wp_kses_post($plugin['downloaded_formatted']); ?></span>
207 - <?php endif; ?>
208 -
209 - <div class="rating-section">
210 - <!-- <span class="rating-label"><?php //esc_html_e('Ratings', 'ultimate-post-kit'); ?></span> -->
211 - <div class="wporg-ratings" title="<?php echo esc_attr($plugin['rating'] ?? '0'); ?> out of 5 stars" style="color:var(--wp--preset--color--pomegrade-1, #e26f56);">
212 - <?php
213 - $rating = floatval($plugin['rating'] ?? 0);
214 - $full_stars = floor($rating);
215 - $has_half_star = ($rating - $full_stars) >= 0.5;
216 - $empty_stars = 5 - $full_stars - ($has_half_star ? 1 : 0);
217 -
218 - // Full stars
219 - for ($i = 0; $i < $full_stars; $i++) {
220 - echo '<span class="dashicons dashicons-star-filled"></span>';
221 - }
222 -
223 - // Half star
224 - if ($has_half_star) {
225 - echo '<span class="dashicons dashicons-star-half"></span>';
226 - }
227 -
228 - // Empty stars
229 - for ($i = 0; $i < $empty_stars; $i++) {
230 - echo '<span class="dashicons dashicons-star-empty"></span>';
231 - }
232 - ?>
233 - </div>
234 - <span class="rating-text">
235 - <?php echo esc_html($plugin['rating'] ?? '0'); ?> out of 5 stars.
236 - <?php if (isset($plugin['num_ratings']) && $plugin['num_ratings'] > 0): ?>
237 - <span class="rating-count">(<?php echo number_format($plugin['num_ratings']); ?> ratings)</span>
238 - <?php endif; ?>
239 - </span>
240 - </div>
241 -
242 -
243 -
244 - <?php if (isset($plugin['last_updated']) && !empty($plugin['last_updated'])): ?>
245 - <span class="last-updated"><?php esc_html_e('Last Updated: ', 'ultimate-post-kit'); echo esc_html(format_last_updated($plugin['last_updated'])); ?></span>
246 - <?php endif; ?>
247 -
248 -
249 - </label>
250 - <?php
251 - endforeach; ?>
245 + </label>
246 + <?php
247 + endforeach; ?>
248 + <?php endif; ?>
252 249 </div>
253 250
254 251 <div class="wizard-navigation bdt-margin-top">
255 252 <button class="bdt-button bdt-button-primary d-none" type="submit" id="upk-install-plugins-btn">
@@ -265,4 +262,271 @@
265 262 <?php esc_html_e('Previous Step', 'ultimate-post-kit'); ?>
266 263 </button>
267 264 </div>
268 265 </div>
266 +
267 +<style>
268 +.upk-loading-dots {
269 + display: flex;
270 + justify-content: center;
271 + gap: 8px;
272 + margin: 20px 0;
273 +}
274 +
275 +.upk-loading-dot {
276 + width: 12px;
277 + height: 12px;
278 + background-color: #0073aa;
279 + border-radius: 50%;
280 + animation: upk-wave 1.4s ease-in-out infinite both;
281 +}
282 +
283 +.upk-loading-dot:nth-child(1) {
284 + animation-delay: -0.32s;
285 +}
286 +
287 +.upk-loading-dot:nth-child(2) {
288 + animation-delay: -0.16s;
289 +}
290 +
291 +.upk-loading-dot:nth-child(3) {
292 + animation-delay: 0s;
293 +}
294 +
295 +@keyframes upk-wave {
296 + 0%, 80%, 100% {
297 + transform: scale(0.8);
298 + opacity: 0.5;
299 + }
300 + 40% {
301 + transform: scale(1.2);
302 + opacity: 1;
303 + }
304 +}
305 +</style>
306 +
307 +<script>
308 +jQuery(document).ready(function($) {
309 + let integrationDataLoaded = false;
310 +
311 + // Function to load integration data
312 + function loadIntegrationData() {
313 + if (integrationDataLoaded) return;
314 +
315 + const $pluginList = $('#upk-integration-plugin-list');
316 + const $initialLoading = $('#upk-initial-loading');
317 +
318 + // Don't add another loading state if initial loading is visible
319 + // Just keep the existing one
320 +
321 + // Make AJAX request to get plugin data
322 + $.ajax({
323 + url: ajaxurl,
324 + type: 'POST',
325 + data: {
326 + action: 'upk_get_plugins',
327 + nonce: '<?php echo esc_attr( wp_create_nonce('upk_get_plugins_nonce') ); ?>'
328 + },
329 + success: function(response) {
330 + if (response.success && response.data.plugins) {
331 + // Hide the initial loading div
332 + $initialLoading.hide();
333 + renderPluginList(response.data.plugins);
334 + integrationDataLoaded = true;
335 + } else {
336 + showError('Unable to load plugin data.');
337 + }
338 + },
339 + error: function() {
340 + showError('Network error occurred while loading plugin data.');
341 + }
342 + });
343 + }
344 +
345 + // Translatable strings used by the dynamically rendered plugin list.
346 + const upkI18n = <?php echo wp_json_encode( array(
347 + 'noPlugins' => __( 'No plugins found.', 'ultimate-post-kit' ),
348 + 'recommended' => __( 'Recommended', 'ultimate-post-kit' ),
349 + 'active' => __( 'ACTIVE', 'ultimate-post-kit' ),
350 + /* translators: %s: number of active installs, or the "Fewer than 10" phrase. */
351 + 'activeInstalls' => __( 'Active Installs: %s', 'ultimate-post-kit' ),
352 + 'fewerThanTen' => __( 'Fewer than 10', 'ultimate-post-kit' ),
353 + /* translators: %s: formatted download count. */
354 + 'downloads' => __( 'Downloads: %s', 'ultimate-post-kit' ),
355 + /* translators: %s: plugin rating, e.g. 4.5. */
356 + 'ratingTitle' => __( '%s out of 5 stars', 'ultimate-post-kit' ),
357 + /* translators: %s: plugin rating, e.g. 4.5. */
358 + 'ratingText' => __( '%s out of 5 stars.', 'ultimate-post-kit' ),
359 + /* translators: %s: number of ratings. */
360 + 'ratingCount' => __( '(%s ratings)', 'ultimate-post-kit' ),
361 + /* translators: %s: how long ago the plugin was updated. */
362 + 'lastUpdated' => __( 'Last Updated: %s', 'ultimate-post-kit' ),
363 + ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); ?>;
364 +
365 + // Minimal printf-style substitution so translators keep control of word order.
366 + function upkFormat(template, value) {
367 + return String(template).replace('%s', value);
368 + }
369 +
370 + // Function to render plugin list
371 + function renderPluginList(plugins) {
372 + const $pluginList = $('#upk-integration-plugin-list');
373 + let html = '';
374 +
375 + if (plugins.length === 0) {
376 + html = `<div class="upk-no-plugins" style="text-align: center; padding: 40px;"><p>${upkEsc(upkI18n.noPlugins)}</p></div>`;
377 + } else {
378 + plugins.forEach(function(plugin) {
379 + // Skip own plugin (Ultimate Post Kit) when printing only; data still includes it for other plugins
380 + if (plugin.slug === 'ultimate-post-kit') return;
381 + const isActive = plugin.status === 'active';
382 + const isRecommended = plugin.recommended && !isActive;
383 +
384 + html += `
385 + <label class="plugin-item" data-slug="${upkEsc(plugin.slug)}">
386 + <span class="bdt-flex bdt-flex-middle bdt-flex-between bdt-margin-small-bottom">
387 + <span class="bdt-plugin-logo">
388 + ${generatePluginLogo(plugin)}
389 + </span>
390 + <div class="bdt-plugin-badge-switch-wrap">
391 + ${isRecommended ? `<span class="recommended-badge">${upkEsc(upkI18n.recommended)}</span>` : ''}
392 + ${isActive ? `<span class="active-badge">${upkEsc(upkI18n.active)}</span>` : ''}
393 + ${!isActive ? `
394 + <label class="switch">
395 + <input type="checkbox" class="plugin-slider-checkbox" ${plugin.recommended ? 'checked' : ''} name="plugins[]${upkEsc(plugin.slug)}">
396 + <span class="slider round"></span>
397 + </label>
398 + ` : ''}
399 + </div>
400 + </span>
401 + <div class="bdt-flex bdt-flex-middle">
402 + <span class="bdt-plugin-name">${upkEsc(plugin.name)}</span>
403 + </div>
404 + <span class="active-installs">
405 + ${upkFormat(upkEsc(upkI18n.activeInstalls), `<span class="installs-count">${plugin.active_installs_count > 0 ? upkEsc(plugin.active_installs_count.toLocaleString() + '+') : upkEsc(upkI18n.fewerThanTen)}</span>`)}
406 + </span>
407 + ${plugin.downloaded_formatted ? `<span class="downloads">${upkFormat(upkEsc(upkI18n.downloads), upkEsc(plugin.downloaded_formatted))}</span>` : ''}
408 + <div class="rating-section">
409 + <div class="wporg-ratings" title="${upkEsc(upkFormat(upkI18n.ratingTitle, plugin.rating))}" style="color:var(--wp--preset--color--pomegrade-1, #e26f56);">
410 + ${generateStarRating(plugin.rating)}
411 + </div>
412 + <span class="rating-text">
413 + ${upkEsc(upkFormat(upkI18n.ratingText, plugin.rating))}
414 + ${plugin.num_ratings > 0 ? `<span class="rating-count">${upkEsc(upkFormat(upkI18n.ratingCount, plugin.num_ratings.toLocaleString()))}</span>` : ''}
415 + </span>
416 + </div>
417 + ${plugin.last_updated_formatted ? `<span class="last-updated">${upkFormat(upkEsc(upkI18n.lastUpdated), upkEsc(plugin.last_updated_formatted))}</span>` : ''}
418 + </label>
419 + `;
420 + });
421 + }
422 +
423 + $pluginList.html(html);
424 + }
425 +
426 + // Escape remote-sourced strings before they are concatenated into markup. The plugin
427 + // catalog comes from a remote endpoint; treat it as untrusted so a poisoned or
428 + // compromised feed cannot inject HTML/JS into the admin dashboard. Note that
429 + // Remote_Data_Handler::decode_api_text() html_entity_decode()s these fields, so they
430 + // arrive here already un-escaped.
431 + function upkEsc(s) {
432 + return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
433 + return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
434 + });
435 + }
436 +
437 + function upkSafeUrl(u) {
438 + u = String(u == null ? '' : u);
439 + return /^https?:\/\//i.test(u) ? u : '';
440 + }
441 +
442 + // Helper function to generate plugin logo
443 + function generatePluginLogo(plugin) {
444 + if (plugin.logo && plugin.logo.match(/^https?:\/\//)) {
445 + return `<img src="${upkEsc(upkSafeUrl(plugin.logo))}" alt="${upkEsc(plugin.name)}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
446 + <div class="default-plugin-icon" style="display:none;">📦</div>`;
447 + } else {
448 + // No icon supplied by the data source — show the local placeholder
449 + // rather than offloading an image request to a remote host.
450 + return `<div class="default-plugin-icon" style="display:flex;">📦</div>`;
451 + }
452 + }
453 +
454 + // Helper function to generate star rating
455 + function generateStarRating(rating) {
456 + const fullStars = Math.floor(rating);
457 + const hasHalfStar = (rating - fullStars) >= 0.5;
458 + const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
459 +
460 + let html = '';
461 + for (let i = 0; i < fullStars; i++) {
462 + html += '<span class="dashicons dashicons-star-filled"></span>';
463 + }
464 + if (hasHalfStar) {
465 + html += '<span class="dashicons dashicons-star-half"></span>';
466 + }
467 + for (let i = 0; i < emptyStars; i++) {
468 + html += '<span class="dashicons dashicons-star-empty"></span>';
469 + }
470 + return html;
471 + }
472 +
473 + // Function to show error
474 + function showError(message) {
475 + const $pluginList = $('#upk-integration-plugin-list');
476 + const $initialLoading = $('#upk-initial-loading');
477 +
478 + // Hide initial loading
479 + $initialLoading.hide();
480 +
481 + // Show error in plugin list
482 + $pluginList.html(`
483 + <div class="upk-error-state" style="text-align: center; padding: 40px;">
484 + <p style="color: #d63638;">${upkEsc(message)}</p>
485 + <button type="button" class="bdt-button bdt-button-secondary" onclick="location.reload()">Retry</button>
486 + </div>
487 + `);
488 + }
489 +
490 + // Detect when integration tab becomes active
491 + function observeIntegrationTab() {
492 + // Check if integration step is currently visible
493 + const $integrationStep = $('.bdt-setup-wizard-integration');
494 +
495 + if ($integrationStep.length) {
496 + // Create a MutationObserver to detect when the integration step becomes visible
497 + const observer = new MutationObserver(function(mutations) {
498 + mutations.forEach(function(mutation) {
499 + if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
500 + const $target = $(mutation.target);
501 + if ($target.hasClass('bdt-setup-wizard-integration') && $target.is(':visible')) {
502 + loadIntegrationData();
503 + observer.disconnect(); // Stop observing after first load
504 + }
505 + }
506 + });
507 + });
508 +
509 + // Start observing
510 + observer.observe($integrationStep[0], {
511 + attributes: true,
512 + attributeFilter: ['class']
513 + });
514 +
515 + // Also check immediately if it's already visible
516 + if ($integrationStep.is(':visible') && !integrationDataLoaded) {
517 + loadIntegrationData();
518 + }
519 + }
520 + }
521 +
522 + // Initialize tab observation
523 + observeIntegrationTab();
524 +
525 + // Fallback: Also try to detect tab clicks (for different wizard implementations)
526 + $(document).on('click', '[data-step="integration"], .bdt-wizard-step[data-step="integration"]', function() {
527 + if (!integrationDataLoaded) {
528 + setTimeout(loadIntegrationData, 100);
529 + }
530 + });
531 +});
532 +</script>