PluginProbe
Icon Element – Icon Pack for Elementor Page Builder (6718 icons) / trunk
Icon Element – Icon Pack for Elementor Page Builder (6718 icons) vtrunk
icon-element / admin / assets / js / admin-script.js

admin-script.js in Icon Element – Icon Pack for Elementor Page Builder (6718 icons) trunk, at admin/assets/js/admin-script.js

503 lines 20.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * React Admin Panel - Admin Script
3 */
4 (function(wp) {
5 const { createElement: el, useState, useEffect, useRef } = wp.element;
6 const { ToggleControl, TabPanel } = wp.components || {};
7 const apiFetch = wp.apiFetch;
8
9 const hasComponents = !!wp.components;
10 const API_NAMESPACE = rapSettings.restNamespace;
11
12 // ── Toast notifications ────────────────────────────────────────────────────
13
14 const ToastContainer = ({ toasts }) => {
15 if (!toasts.length) return null;
16 return el('div', { className: 'rap-toast-container' },
17 toasts.map(t =>
18 el('div', { key: t.id, className: `rap-toast rap-toast-${t.type}` },
19 el('span', { className: 'rap-toast-icon' }, t.type === 'success' ? '' : ''),
20 el('span', {}, t.message)
21 )
22 )
23 );
24 };
25
26 // ── Fallback tab bar ───────────────────────────────────────────────────────
27
28 const FallbackTabs = ({ tabs, activeTab, onSelect }) =>
29 el('div', { className: 'rap-nav-tabs' },
30 tabs.map(tab =>
31 el('button', {
32 key: tab.name,
33 className: `rap-nav-tab${activeTab === tab.name ? ' is-active' : ''}`,
34 onClick: () => onSelect(tab.name)
35 }, tab.title)
36 )
37 );
38
39 // ── Toggle item ────────────────────────────────────────────────────────────
40
41 const ToggleItem = ({ id, name, checked, onChange, previewUrl, locked }) => {
42 const inner = hasComponents && ToggleControl
43 ? el(ToggleControl, {
44 label: name,
45 checked,
46 onChange: val => onChange(id, val),
47 disabled: locked // disable toggle when locked
48 })
49 : el('label', { className: 'rap-toggle-switch' },
50 el('input', {
51 type: 'checkbox',
52 checked,
53 onChange: e => onChange(id, e.target.checked),
54 disabled: locked // disable checkbox when locked
55 }),
56 el('span', { className: 'rap-toggle-track' },
57 el('span', { className: 'rap-toggle-thumb' })
58 ),
59 el('span', { className: 'rap-toggle-label' }, name)
60 );
61
62 // 2. Add class "rap-toggle-locked" when locked
63 return el('div', {
64 className: `rap-toggle-item${checked ? ' is-enabled' : ''}${locked ? ' rap-toggle-locked' : ''}`
65 },
66 el('div', { className: 'rap-toggle-item-inner' }, inner),
67 previewUrl && el('a', {
68 href: previewUrl,
69 target: '_blank',
70 rel: 'noopener noreferrer',
71 className: 'rap-preview-btn'
72 }, 'Preview ↗')
73 );
74 };
75
76 // ── Plugin card helpers ────────────────────────────────────────────────────
77
78 const decodeHtml = (str) => {
79 if (!str) return str;
80 const txt = document.createElement('textarea');
81 txt.innerHTML = str;
82 return txt.value;
83 };
84
85 const StarRating = ({ rating, numRatings }) => {
86 if (!numRatings) return null;
87 const filled = Math.round((rating || 0) / 20); // 0-100 → 0-5
88 return el('div', { className: 'rap-star-rating' },
89 [1, 2, 3, 4, 5].map(i =>
90 el('span', { key: i, className: `rap-star${i <= filled ? ' rap-star-on' : ''}` }, '�
91 ')
92 )
93 );
94 };
95
96 const PluginCard = ({ plugin, onInstall, onActivate, isInstalling, buttonStates }) => {
97 const [imgError, setImgError] = useState(false);
98 const slug = plugin.slug || '';
99 const name = decodeHtml(plugin.name || slug || 'Unknown Plugin');
100 const desc = decodeHtml(plugin.short_description || '');
101 const version = plugin.version || '';
102 const author = plugin.author || '';
103 const rating = plugin.rating || 0;
104 const numRatings = plugin.num_ratings || 0;
105 const pluginUrl = plugin.plugin_url || `https://wordpress.org/plugins/${slug}/`;
106 const state = buttonStates[slug] || {};
107
108 const requiresPlugins = plugin.requires_plugins || [];
109 const requiresWoo = requiresPlugins.some(p => /woocommerce/i.test(p))
110 || /woocommerce/i.test(name);
111
112 let action;
113 if (state.active) {
114 action = el('span', { className: 'rap-badge rap-badge-active' },
115 el('span', { className: 'rap-badge-dot' }),
116 'Active'
117 );
118 } else if (state.installed) {
119 action = el('button', {
120 className: 'rap-btn rap-btn-outline',
121 onClick: () => onActivate(slug),
122 disabled: isInstalling
123 }, isInstalling
124 ? el('span', { className: 'rap-btn-inner' },
125 el('span', { className: 'rap-spinner-sm' }), 'Activating…'
126 )
127 : 'Activate'
128 );
129 } else {
130 action = el('button', {
131 className: 'rap-btn rap-btn-primary',
132 onClick: () => onInstall(slug),
133 disabled: isInstalling
134 }, isInstalling
135 ? el('span', { className: 'rap-btn-inner' },
136 el('span', { className: 'rap-spinner-sm' }), 'Installing…'
137 )
138 : 'Install'
139 );
140 }
141
142 const iconEl = plugin.thumbnail && !imgError
143 ? el('img', {
144 src: plugin.thumbnail,
145 alt: name,
146 className: 'rap-plugin-icon',
147 onError: () => setImgError(true)
148 })
149 : el('div', { className: 'rap-plugin-icon-fallback' }, name.charAt(0).toUpperCase());
150
151 return el('div', { className: `rap-plugin-card${state.active ? ' is-active' : state.installed ? ' is-installed' : ''}` },
152 el('div', { className: 'rap-plugin-card-top' },
153 el('div', { className: 'rap-plugin-icon-wrap' }, iconEl),
154 el('div', { className: 'rap-plugin-header' },
155 el('h3', { className: 'rap-plugin-name' }, name)
156 )
157 ),
158 el('div', { className: 'rap-plugin-body' },
159 desc && el('p', { className: 'rap-plugin-desc' }, desc),
160 requiresWoo && el('span', { className: 'rap-woo-badge' },
161 el('span', { className: 'rap-woo-icon' }, 'W'),
162 'Requires WooCommerce'
163 ),
164 el('a', {
165 href: pluginUrl,
166 target: '_blank',
167 rel: 'noopener noreferrer',
168 className: 'rap-details-link'
169 }, 'View Details ↗')
170 ),
171 el('div', { className: 'rap-plugin-card-footer' },
172 el(StarRating, { rating, numRatings }),
173 action
174 )
175 );
176 };
177
178 // ── Main App ───────────────────────────────────────────────────────────────
179
180 const App = () => {
181 const [settings, setSettings] = useState({});
182 const [iconSets, setIconSets] = useState([]);
183 const [activeTab, setActiveTab] = useState('general');
184 const [isSaving, setIsSaving] = useState(false);
185 const [isLoading, setIsLoading] = useState(true);
186 const [loadError, setLoadError] = useState(null);
187 const [toasts, setToasts] = useState([]);
188 const [search, setSearch] = useState('');
189 const [plugins, setPlugins] = useState([]);
190 const [pluginsLoading, setPluginsLoading] = useState(false);
191 const [pluginsFailed, setPluginsFailed] = useState(false);
192 const [pluginStatuses, setPluginStatuses] = useState({});
193 const [installingSlug, setInstallingSlug] = useState(null);
194 const toastId = useRef(0);
195 const authors = rapSettings.authorSlugs || ['softtent'];
196 const is_pro = rapSettings.ispro ;
197
198 const addToast = (message, type = 'success') => {
199 const id = ++toastId.current;
200 setToasts(prev => [...prev, { id, message, type }]);
201 setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 4000);
202 };
203
204 // Load settings on mount
205 useEffect(() => {
206 (async () => {
207 try {
208 setIconSets(rapSettings.iconSets || []);
209 const data = await apiFetch({
210 path: `/${API_NAMESPACE}/settings`,
211 method: 'GET',
212 headers: { 'X-WP-Nonce': rapSettings.nonce }
213 });
214 setSettings(data);
215 } catch (_) {
216 setLoadError('Could not load settings. Please reload the page.');
217 } finally {
218 setIsLoading(false);
219 }
220 })();
221 }, []);
222
223 // Clear search and reset plugin error when switching tabs
224 useEffect(() => {
225 setSearch('');
226 if (activeTab !== 'recommended-plugins') setPluginsFailed(false);
227 }, [activeTab]);
228
229 // Load plugins when recommended-plugins tab is first opened
230 useEffect(() => {
231 if (activeTab === 'recommended-plugins' && !plugins.length && !pluginsLoading) {
232 fetchPlugins();
233 }
234 }, [activeTab]);
235
236 const fetchPlugins = async () => {
237 setPluginsLoading(true);
238 setPluginsFailed(false);
239 try {
240 const data = await apiFetch({
241 path: `/${API_NAMESPACE}/plugins?authors=${encodeURIComponent(authors.join(','))}`,
242 method: 'GET',
243 headers: { 'X-WP-Nonce': rapSettings.nonce }
244 });
245
246 const raw = Array.isArray(data) ? data
247 : data && data.plugins ? data.plugins
248 : data && typeof data === 'object' ? Object.values(data).filter(p => p && typeof p === 'object')
249 : [];
250
251 // Pin softtent-managex to the top of the list
252 const list = [
253 ...raw.filter(p => p.slug === 'softtent-managex'),
254 ...raw.filter(p => p.slug !== 'softtent-managex')
255 ];
256 setPlugins(list);
257
258 const statuses = {};
259 await Promise.all(list.map(async p => {
260 if (!p.slug) return;
261 try {
262 statuses[p.slug] = await apiFetch({
263 path: `/${API_NAMESPACE}/plugin-status?slug=${encodeURIComponent(p.slug)}`,
264 method: 'GET',
265 headers: { 'X-WP-Nonce': rapSettings.nonce }
266 });
267 } catch (_) {
268 statuses[p.slug] = { installed: false, active: false };
269 }
270 }));
271 setPluginStatuses(statuses);
272 } catch (_) {
273 // Only mark failed on a real REST/network exception
274 setPluginsFailed(true);
275 } finally {
276 setPluginsLoading(false);
277 }
278 };
279
280 const handleInstall = async (slug) => {
281 setInstallingSlug(slug);
282 try {
283 await apiFetch({
284 path: `/${API_NAMESPACE}/install-plugin`,
285 method: 'POST',
286 headers: { 'X-WP-Nonce': rapSettings.nonce, 'Content-Type': 'application/json' },
287 body: JSON.stringify({ slug })
288 });
289 const status = await apiFetch({
290 path: `/${API_NAMESPACE}/plugin-status?slug=${encodeURIComponent(slug)}`,
291 method: 'GET',
292 headers: { 'X-WP-Nonce': rapSettings.nonce }
293 });
294 setPluginStatuses(prev => ({ ...prev, [slug]: status }));
295 addToast('Plugin installed successfully!');
296 } catch (e) {
297 addToast('Install failed: ' + (e.message || 'Unknown error'), 'error');
298 } finally {
299 setInstallingSlug(null);
300 }
301 };
302
303 const handleActivate = async (slug) => {
304 setInstallingSlug(slug);
305 try {
306 await apiFetch({
307 path: `/${API_NAMESPACE}/activate-plugin`,
308 method: 'POST',
309 headers: { 'X-WP-Nonce': rapSettings.nonce, 'Content-Type': 'application/json' },
310 body: JSON.stringify({ slug })
311 });
312 const status = await apiFetch({
313 path: `/${API_NAMESPACE}/plugin-status?slug=${encodeURIComponent(slug)}`,
314 method: 'GET',
315 headers: { 'X-WP-Nonce': rapSettings.nonce }
316 });
317 setPluginStatuses(prev => ({ ...prev, [slug]: status }));
318 addToast('Plugin activated!');
319 } catch (e) {
320 addToast('Activation failed: ' + (e.message || 'Unknown error'), 'error');
321 } finally {
322 setInstallingSlug(null);
323 }
324 };
325
326 const handleSave = async () => {
327 setIsSaving(true);
328 try {
329 const response = await apiFetch({
330 path: `/${API_NAMESPACE}/settings`,
331 method: 'POST',
332 headers: { 'X-WP-Nonce': rapSettings.nonce, 'Content-Type': 'application/json' },
333 body: JSON.stringify(settings)
334 });
335 setSettings(response);
336 addToast('Settings saved!');
337 } catch (_) {
338 addToast('Save failed. Please try again.', 'error');
339 } finally {
340 setIsSaving(false);
341 }
342 };
343
344 const freeIcons = iconSets.filter(i => !i.pro);
345 const proIcons = iconSets.filter(i => i.pro);
346
347 const renderIconGrid = (icons) => {
348 const enabledCount = icons.filter(i => settings[i.id]).length;
349 const filtered = search
350 ? icons.filter(i => i.name.toLowerCase().includes(search.toLowerCase()))
351 : icons;
352
353 return el('div', {},
354 el('div', { className: 'rap-toolbar' },
355 el('div', { className: 'rap-chips' },
356 el('span', { className: 'rap-chip rap-chip-primary' }, `${enabledCount} enabled`),
357 el('span', { className: 'rap-chip' }, `${icons.length} total`)
358 ),
359 el('input', {
360 type: 'text',
361 className: 'rap-search',
362 placeholder: 'Search icon sets…',
363 value: search,
364 onChange: e => setSearch(e.target.value)
365 })
366 ),
367 filtered.length === 0
368 ? el('p', { className: 'rap-empty' }, `No results for "${search}"`)
369 : el('div', { className: 'rap-icon-grid' },
370 filtered.map(icon => {
371 const locked = icon.pro && !is_pro; // compute locked flag
372 return el(ToggleItem, {
373 key: icon.id,
374 id: icon.id,
375 name: icon.name,
376 checked: !!settings[icon.id],
377 onChange: (id, val) => setSettings(prev => ({ ...prev, [id]: val })),
378 previewUrl: icon.preview,
379 locked // pass locked prop
380 });
381 })
382 )
383 );
384 };
385
386 const renderPluginsTab = () => {
387 if (pluginsLoading) {
388 return el('div', { className: 'rap-loading' },
389 el('span', { className: 'rap-spinner' }),
390 el('span', {}, 'Loading plugins…')
391 );
392 }
393 if (pluginsFailed) {
394 return el('div', { className: 'rap-empty rap-empty-center' },
395 el('span', { className: 'rap-empty-icon' }, '⚠️'),
396 el('p', {}, 'Plugins could not be loaded.'),
397 el('button', {
398 className: 'rap-btn rap-btn-primary',
399 onClick: fetchPlugins
400 }, 'Try Again')
401 );
402 }
403 if (!plugins.length) {
404 return el('div', { className: 'rap-empty rap-empty-center' },
405 el('span', { className: 'rap-empty-icon' }, '📦'),
406 el('p', {}, 'No plugins found.')
407 );
408 }
409 const statVals = Object.values(pluginStatuses);
410 const activeCount = statVals.filter(s => s.active).length;
411 const readyCount = statVals.filter(s => s.installed && !s.active).length;
412
413 return el('div', {},
414 el('div', { className: 'rap-toolbar' },
415 el('div', { className: 'rap-chips' },
416 el('span', { className: 'rap-chip rap-chip-success' }, `${activeCount} active`),
417 readyCount > 0 && el('span', { className: 'rap-chip rap-chip-warning' }, `${readyCount} installed`),
418 el('span', { className: 'rap-chip' }, `${plugins.length} total`)
419 )
420 ),
421 el('div', { className: 'rap-plugins-grid' },
422 plugins.map(p =>
423 el(PluginCard, {
424 key: p.slug,
425 plugin: p,
426 onInstall: handleInstall,
427 onActivate: handleActivate,
428 isInstalling: installingSlug === p.slug,
429 buttonStates: pluginStatuses
430 })
431 )
432 )
433 );
434 };
435
436 if (isLoading) {
437 return el('div', { className: 'rap-loading' },
438 el('span', { className: 'rap-spinner' }),
439 el('span', {}, 'Loading…')
440 );
441 }
442 if (loadError) {
443 return el('div', { className: 'rap-notice rap-notice-error' }, loadError);
444 }
445
446 const tabs = [
447 { name: 'general', title: `Free Icons (${freeIcons.length} Sets)` },
448 { name: 'advanced', title: `Pro Icons (${proIcons.length} Sets)` },
449 { name: 'recommended-plugins', title: 'Recommended Plugins' }
450 ];
451
452 const getTabContent = name => {
453 if (name === 'general') return renderIconGrid(freeIcons);
454 if (name === 'advanced') return renderIconGrid(proIcons);
455 if (name === 'recommended-plugins') return renderPluginsTab();
456 return null;
457 };
458
459 const showSave = activeTab === 'general' || activeTab === 'advanced';
460
461 return el('div', { className: 'rap-app' },
462 el(ToastContainer, { toasts }),
463 el('div', { className: 'rap-panel' },
464 hasComponents && TabPanel
465 ? el(TabPanel, {
466 className: 'rap-tabs',
467 activeClass: 'is-active',
468 tabs,
469 onSelect: setActiveTab,
470 children: tab => el('div', { className: 'rap-tab-body' }, getTabContent(tab.name))
471 })
472 : el('div', {},
473 el(FallbackTabs, { tabs, activeTab, onSelect: setActiveTab }),
474 el('div', { className: 'rap-tab-body' }, getTabContent(activeTab))
475 ),
476 showSave && el('div', { className: 'rap-footer' },
477 el('button', {
478 className: `rap-btn rap-btn-primary rap-btn-lg${isSaving ? ' is-loading' : ''}`,
479 onClick: handleSave,
480 disabled: isSaving
481 }, isSaving
482 ? el('span', { className: 'rap-btn-inner' },
483 el('span', { className: 'rap-spinner-sm' }),
484 'Saving…'
485 )
486 : 'Save Settings'
487 )
488 )
489 )
490 );
491 };
492
493 // Mount
494 const root = document.getElementById('rap-root');
495 if (root) {
496 if (wp.element.createRoot) {
497 wp.element.createRoot(root).render(el(App));
498 } else {
499 wp.element.render(el(App), root);
500 }
501 }
502
503 })(window.wp);