/** * React Admin Panel - Admin Script */ (function(wp) { const { createElement: el, useState, useEffect, useRef } = wp.element; const { ToggleControl, TabPanel } = wp.components || {}; const apiFetch = wp.apiFetch; const hasComponents = !!wp.components; const API_NAMESPACE = rapSettings.restNamespace; // ── Toast notifications ──────────────────────────────────────────────────── const ToastContainer = ({ toasts }) => { if (!toasts.length) return null; return el('div', { className: 'rap-toast-container' }, toasts.map(t => el('div', { key: t.id, className: `rap-toast rap-toast-${t.type}` }, el('span', { className: 'rap-toast-icon' }, t.type === 'success' ? '✓' : '✕'), el('span', {}, t.message) ) ) ); }; // ── Fallback tab bar ─────────────────────────────────────────────────────── const FallbackTabs = ({ tabs, activeTab, onSelect }) => el('div', { className: 'rap-nav-tabs' }, tabs.map(tab => el('button', { key: tab.name, className: `rap-nav-tab${activeTab === tab.name ? ' is-active' : ''}`, onClick: () => onSelect(tab.name) }, tab.title) ) ); // ── Toggle item ──────────────────────────────────────────────────────────── const ToggleItem = ({ id, name, checked, onChange, previewUrl, locked }) => { const inner = hasComponents && ToggleControl ? el(ToggleControl, { label: name, checked, onChange: val => onChange(id, val), disabled: locked // disable toggle when locked }) : el('label', { className: 'rap-toggle-switch' }, el('input', { type: 'checkbox', checked, onChange: e => onChange(id, e.target.checked), disabled: locked // disable checkbox when locked }), el('span', { className: 'rap-toggle-track' }, el('span', { className: 'rap-toggle-thumb' }) ), el('span', { className: 'rap-toggle-label' }, name) ); // 2. Add class "rap-toggle-locked" when locked return el('div', { className: `rap-toggle-item${checked ? ' is-enabled' : ''}${locked ? ' rap-toggle-locked' : ''}` }, el('div', { className: 'rap-toggle-item-inner' }, inner), previewUrl && el('a', { href: previewUrl, target: '_blank', rel: 'noopener noreferrer', className: 'rap-preview-btn' }, 'Preview ↗') ); }; // ── Plugin card helpers ──────────────────────────────────────────────────── const decodeHtml = (str) => { if (!str) return str; const txt = document.createElement('textarea'); txt.innerHTML = str; return txt.value; }; const StarRating = ({ rating, numRatings }) => { if (!numRatings) return null; const filled = Math.round((rating || 0) / 20); // 0-100 → 0-5 return el('div', { className: 'rap-star-rating' }, [1, 2, 3, 4, 5].map(i => el('span', { key: i, className: `rap-star${i <= filled ? ' rap-star-on' : ''}` }, '★') ) ); }; const PluginCard = ({ plugin, onInstall, onActivate, isInstalling, buttonStates }) => { const [imgError, setImgError] = useState(false); const slug = plugin.slug || ''; const name = decodeHtml(plugin.name || slug || 'Unknown Plugin'); const desc = decodeHtml(plugin.short_description || ''); const version = plugin.version || ''; const author = plugin.author || ''; const rating = plugin.rating || 0; const numRatings = plugin.num_ratings || 0; const pluginUrl = plugin.plugin_url || `https://wordpress.org/plugins/${slug}/`; const state = buttonStates[slug] || {}; const requiresPlugins = plugin.requires_plugins || []; const requiresWoo = requiresPlugins.some(p => /woocommerce/i.test(p)) || /woocommerce/i.test(name); let action; if (state.active) { action = el('span', { className: 'rap-badge rap-badge-active' }, el('span', { className: 'rap-badge-dot' }), 'Active' ); } else if (state.installed) { action = el('button', { className: 'rap-btn rap-btn-outline', onClick: () => onActivate(slug), disabled: isInstalling }, isInstalling ? el('span', { className: 'rap-btn-inner' }, el('span', { className: 'rap-spinner-sm' }), 'Activating…' ) : 'Activate' ); } else { action = el('button', { className: 'rap-btn rap-btn-primary', onClick: () => onInstall(slug), disabled: isInstalling }, isInstalling ? el('span', { className: 'rap-btn-inner' }, el('span', { className: 'rap-spinner-sm' }), 'Installing…' ) : 'Install' ); } const iconEl = plugin.thumbnail && !imgError ? el('img', { src: plugin.thumbnail, alt: name, className: 'rap-plugin-icon', onError: () => setImgError(true) }) : el('div', { className: 'rap-plugin-icon-fallback' }, name.charAt(0).toUpperCase()); return el('div', { className: `rap-plugin-card${state.active ? ' is-active' : state.installed ? ' is-installed' : ''}` }, el('div', { className: 'rap-plugin-card-top' }, el('div', { className: 'rap-plugin-icon-wrap' }, iconEl), el('div', { className: 'rap-plugin-header' }, el('h3', { className: 'rap-plugin-name' }, name) ) ), el('div', { className: 'rap-plugin-body' }, desc && el('p', { className: 'rap-plugin-desc' }, desc), requiresWoo && el('span', { className: 'rap-woo-badge' }, el('span', { className: 'rap-woo-icon' }, 'W'), 'Requires WooCommerce' ), el('a', { href: pluginUrl, target: '_blank', rel: 'noopener noreferrer', className: 'rap-details-link' }, 'View Details ↗') ), el('div', { className: 'rap-plugin-card-footer' }, el(StarRating, { rating, numRatings }), action ) ); }; // ── Main App ─────────────────────────────────────────────────────────────── const App = () => { const [settings, setSettings] = useState({}); const [iconSets, setIconSets] = useState([]); const [activeTab, setActiveTab] = useState('general'); const [isSaving, setIsSaving] = useState(false); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [toasts, setToasts] = useState([]); const [search, setSearch] = useState(''); const [plugins, setPlugins] = useState([]); const [pluginsLoading, setPluginsLoading] = useState(false); const [pluginsFailed, setPluginsFailed] = useState(false); const [pluginStatuses, setPluginStatuses] = useState({}); const [installingSlug, setInstallingSlug] = useState(null); const toastId = useRef(0); const authors = rapSettings.authorSlugs || ['softtent']; const is_pro = rapSettings.ispro ; const addToast = (message, type = 'success') => { const id = ++toastId.current; setToasts(prev => [...prev, { id, message, type }]); setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 4000); }; // Load settings on mount useEffect(() => { (async () => { try { setIconSets(rapSettings.iconSets || []); const data = await apiFetch({ path: `/${API_NAMESPACE}/settings`, method: 'GET', headers: { 'X-WP-Nonce': rapSettings.nonce } }); setSettings(data); } catch (_) { setLoadError('Could not load settings. Please reload the page.'); } finally { setIsLoading(false); } })(); }, []); // Clear search and reset plugin error when switching tabs useEffect(() => { setSearch(''); if (activeTab !== 'recommended-plugins') setPluginsFailed(false); }, [activeTab]); // Load plugins when recommended-plugins tab is first opened useEffect(() => { if (activeTab === 'recommended-plugins' && !plugins.length && !pluginsLoading) { fetchPlugins(); } }, [activeTab]); const fetchPlugins = async () => { setPluginsLoading(true); setPluginsFailed(false); try { const data = await apiFetch({ path: `/${API_NAMESPACE}/plugins?authors=${encodeURIComponent(authors.join(','))}`, method: 'GET', headers: { 'X-WP-Nonce': rapSettings.nonce } }); const raw = Array.isArray(data) ? data : data && data.plugins ? data.plugins : data && typeof data === 'object' ? Object.values(data).filter(p => p && typeof p === 'object') : []; // Pin softtent-managex to the top of the list const list = [ ...raw.filter(p => p.slug === 'softtent-managex'), ...raw.filter(p => p.slug !== 'softtent-managex') ]; setPlugins(list); const statuses = {}; await Promise.all(list.map(async p => { if (!p.slug) return; try { statuses[p.slug] = await apiFetch({ path: `/${API_NAMESPACE}/plugin-status?slug=${encodeURIComponent(p.slug)}`, method: 'GET', headers: { 'X-WP-Nonce': rapSettings.nonce } }); } catch (_) { statuses[p.slug] = { installed: false, active: false }; } })); setPluginStatuses(statuses); } catch (_) { // Only mark failed on a real REST/network exception setPluginsFailed(true); } finally { setPluginsLoading(false); } }; const handleInstall = async (slug) => { setInstallingSlug(slug); try { await apiFetch({ path: `/${API_NAMESPACE}/install-plugin`, method: 'POST', headers: { 'X-WP-Nonce': rapSettings.nonce, 'Content-Type': 'application/json' }, body: JSON.stringify({ slug }) }); const status = await apiFetch({ path: `/${API_NAMESPACE}/plugin-status?slug=${encodeURIComponent(slug)}`, method: 'GET', headers: { 'X-WP-Nonce': rapSettings.nonce } }); setPluginStatuses(prev => ({ ...prev, [slug]: status })); addToast('Plugin installed successfully!'); } catch (e) { addToast('Install failed: ' + (e.message || 'Unknown error'), 'error'); } finally { setInstallingSlug(null); } }; const handleActivate = async (slug) => { setInstallingSlug(slug); try { await apiFetch({ path: `/${API_NAMESPACE}/activate-plugin`, method: 'POST', headers: { 'X-WP-Nonce': rapSettings.nonce, 'Content-Type': 'application/json' }, body: JSON.stringify({ slug }) }); const status = await apiFetch({ path: `/${API_NAMESPACE}/plugin-status?slug=${encodeURIComponent(slug)}`, method: 'GET', headers: { 'X-WP-Nonce': rapSettings.nonce } }); setPluginStatuses(prev => ({ ...prev, [slug]: status })); addToast('Plugin activated!'); } catch (e) { addToast('Activation failed: ' + (e.message || 'Unknown error'), 'error'); } finally { setInstallingSlug(null); } }; const handleSave = async () => { setIsSaving(true); try { const response = await apiFetch({ path: `/${API_NAMESPACE}/settings`, method: 'POST', headers: { 'X-WP-Nonce': rapSettings.nonce, 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }); setSettings(response); addToast('Settings saved!'); } catch (_) { addToast('Save failed. Please try again.', 'error'); } finally { setIsSaving(false); } }; const freeIcons = iconSets.filter(i => !i.pro); const proIcons = iconSets.filter(i => i.pro); const renderIconGrid = (icons) => { const enabledCount = icons.filter(i => settings[i.id]).length; const filtered = search ? icons.filter(i => i.name.toLowerCase().includes(search.toLowerCase())) : icons; return el('div', {}, el('div', { className: 'rap-toolbar' }, el('div', { className: 'rap-chips' }, el('span', { className: 'rap-chip rap-chip-primary' }, `${enabledCount} enabled`), el('span', { className: 'rap-chip' }, `${icons.length} total`) ), el('input', { type: 'text', className: 'rap-search', placeholder: 'Search icon sets…', value: search, onChange: e => setSearch(e.target.value) }) ), filtered.length === 0 ? el('p', { className: 'rap-empty' }, `No results for "${search}"`) : el('div', { className: 'rap-icon-grid' }, filtered.map(icon => { const locked = icon.pro && !is_pro; // compute locked flag return el(ToggleItem, { key: icon.id, id: icon.id, name: icon.name, checked: !!settings[icon.id], onChange: (id, val) => setSettings(prev => ({ ...prev, [id]: val })), previewUrl: icon.preview, locked // pass locked prop }); }) ) ); }; const renderPluginsTab = () => { if (pluginsLoading) { return el('div', { className: 'rap-loading' }, el('span', { className: 'rap-spinner' }), el('span', {}, 'Loading plugins…') ); } if (pluginsFailed) { return el('div', { className: 'rap-empty rap-empty-center' }, el('span', { className: 'rap-empty-icon' }, '⚠️'), el('p', {}, 'Plugins could not be loaded.'), el('button', { className: 'rap-btn rap-btn-primary', onClick: fetchPlugins }, 'Try Again') ); } if (!plugins.length) { return el('div', { className: 'rap-empty rap-empty-center' }, el('span', { className: 'rap-empty-icon' }, '📦'), el('p', {}, 'No plugins found.') ); } const statVals = Object.values(pluginStatuses); const activeCount = statVals.filter(s => s.active).length; const readyCount = statVals.filter(s => s.installed && !s.active).length; return el('div', {}, el('div', { className: 'rap-toolbar' }, el('div', { className: 'rap-chips' }, el('span', { className: 'rap-chip rap-chip-success' }, `${activeCount} active`), readyCount > 0 && el('span', { className: 'rap-chip rap-chip-warning' }, `${readyCount} installed`), el('span', { className: 'rap-chip' }, `${plugins.length} total`) ) ), el('div', { className: 'rap-plugins-grid' }, plugins.map(p => el(PluginCard, { key: p.slug, plugin: p, onInstall: handleInstall, onActivate: handleActivate, isInstalling: installingSlug === p.slug, buttonStates: pluginStatuses }) ) ) ); }; if (isLoading) { return el('div', { className: 'rap-loading' }, el('span', { className: 'rap-spinner' }), el('span', {}, 'Loading…') ); } if (loadError) { return el('div', { className: 'rap-notice rap-notice-error' }, loadError); } const tabs = [ { name: 'general', title: `Free Icons (${freeIcons.length} Sets)` }, { name: 'advanced', title: `Pro Icons (${proIcons.length} Sets)` }, { name: 'recommended-plugins', title: 'Recommended Plugins' } ]; const getTabContent = name => { if (name === 'general') return renderIconGrid(freeIcons); if (name === 'advanced') return renderIconGrid(proIcons); if (name === 'recommended-plugins') return renderPluginsTab(); return null; }; const showSave = activeTab === 'general' || activeTab === 'advanced'; return el('div', { className: 'rap-app' }, el(ToastContainer, { toasts }), el('div', { className: 'rap-panel' }, hasComponents && TabPanel ? el(TabPanel, { className: 'rap-tabs', activeClass: 'is-active', tabs, onSelect: setActiveTab, children: tab => el('div', { className: 'rap-tab-body' }, getTabContent(tab.name)) }) : el('div', {}, el(FallbackTabs, { tabs, activeTab, onSelect: setActiveTab }), el('div', { className: 'rap-tab-body' }, getTabContent(activeTab)) ), showSave && el('div', { className: 'rap-footer' }, el('button', { className: `rap-btn rap-btn-primary rap-btn-lg${isSaving ? ' is-loading' : ''}`, onClick: handleSave, disabled: isSaving }, isSaving ? el('span', { className: 'rap-btn-inner' }, el('span', { className: 'rap-spinner-sm' }), 'Saving…' ) : 'Save Settings' ) ) ) ); }; // Mount const root = document.getElementById('rap-root'); if (root) { if (wp.element.createRoot) { wp.element.createRoot(root).render(el(App)); } else { wp.element.render(el(App), root); } } })(window.wp);