PluginProbe
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder / 3.1.4
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder v3.1.4
3.1.4 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.0.7 3.0.5 3.0.4 3.0.3 3.0.2 trunk 1.5.0 1.5.1 1.5.2 1.6.1 1.6.2 1.6.3 1.6.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 All 93 releases
ultimate-store-kit / src / admin / pages / WidgetsPage.js

WidgetsPage.js in Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder 3.1.4, at src/admin/pages/WidgetsPage.js

400 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useState, useMemo, useCallback, useEffect } from '@wordpress/element';
2 import { __ } from '@wordpress/i18n';
3 import {
4 CheckIcon,
5 CloseIcon,
6 EyeIcon,
7 LinkPluginIcon,
8 PlayIcon,
9 SearchIcon,
10 } from '../icons';
11 import {
12 btnLg,
13 btnPrimary,
14 proBadge,
15 selectInput,
16 selectLabel,
17 Toggle,
18 } from '../tw';
19 import { isWidgetEffectivelyOn } from '../utils';
20
21 const WidgetsPage = ({
22 allWidgets,
23 allSettings,
24 onSave,
25 saving,
26 isPro,
27 widgetType = 'wc',
28 }) => {
29 const [search, setSearch] = useState('');
30 const [filter, setFilter] = useState('all');
31 const [contentTypeFilter, setContentTypeFilter] = useState('all');
32
33 const widgetTypeConfig = {
34 wc: {
35 title: __('WooCommerce Widgets', 'ultimate-store-kit'),
36 key: 'ultimate_store_kit_active_modules',
37 },
38 edd: {
39 title: __('Easy Digital Downloads Widgets', 'ultimate-store-kit'),
40 key: 'ultimate_store_kit_edd_modules',
41 },
42 other: {
43 title: __('Other Widgets', 'ultimate-store-kit'),
44 key: 'ultimate_store_kit_general_modules',
45 },
46 };
47
48 const currentConfig = widgetTypeConfig[widgetType];
49 const widgets = allWidgets[currentConfig.key] || [];
50 const settings = allSettings[currentConfig.key] || {};
51
52 const [localSettings, setLocalSettings] = useState(() => {
53 const initial = {};
54 Object.keys(allSettings).forEach((sectionKey) => {
55 const sectionSettings = allSettings[sectionKey] || {};
56 const sectionWidgets = allWidgets[sectionKey] || [];
57 sectionWidgets.forEach((w) => {
58 if (w.type !== 'checkbox') return;
59 initial[w.name] =
60 sectionSettings[w.name] !== undefined
61 ? sectionSettings[w.name]
62 : w.default || 'off';
63 });
64 });
65 return initial;
66 });
67
68 useEffect(() => {
69 setSearch('');
70 setFilter('all');
71 setContentTypeFilter('all');
72 }, [widgetType]);
73
74 const contentTypes = useMemo(() => {
75 const types = new Set();
76 widgets.forEach((w) => {
77 if (w.content_type) {
78 w.content_type.split(' ').forEach((t) => {
79 if (
80 t &&
81 t !== 'woocommerce' &&
82 t !== 'edd' &&
83 t !== 'new'
84 ) {
85 types.add(t);
86 }
87 });
88 }
89 });
90 return Array.from(types);
91 }, [widgets]);
92
93 const filteredWidgets = useMemo(() => {
94 return widgets.filter((w) => {
95 if (w.type !== 'checkbox') return false;
96
97 if (search && !w.label.toLowerCase().includes(search.toLowerCase()))
98 return false;
99
100 if (filter === 'free' && w.widget_type !== 'free') return false;
101 if (filter === 'pro' && w.widget_type !== 'pro') return false;
102
103 if (
104 contentTypeFilter !== 'all' &&
105 (!w.content_type || !w.content_type.includes(contentTypeFilter))
106 )
107 return false;
108
109 return true;
110 });
111 }, [widgets, search, filter, contentTypeFilter]);
112
113 const handleToggle = useCallback(
114 (name) => {
115 setLocalSettings((prev) => ({
116 ...prev,
117 [name]: prev[name] === 'on' ? 'off' : 'on',
118 }));
119 },
120 []
121 );
122
123 const handleActivateAll = useCallback(() => {
124 setLocalSettings((prev) => {
125 const updated = { ...prev };
126 filteredWidgets.forEach((w) => {
127 if (w.widget_type === 'pro' && !isPro) return;
128 const dep = w.dependency;
129 if (dep && (!dep.isInstalled || !dep.isActive)) return;
130 updated[w.name] = 'on';
131 });
132 return updated;
133 });
134 }, [filteredWidgets, isPro]);
135
136 const handleDeactivateAll = useCallback(() => {
137 setLocalSettings((prev) => {
138 const updated = { ...prev };
139 filteredWidgets.forEach((w) => {
140 if (w.widget_type === 'pro' && !isPro) return;
141 updated[w.name] = 'off';
142 });
143 return updated;
144 });
145 }, [filteredWidgets, isPro]);
146
147 const handleSave = () => {
148 const currentSettings = {};
149 widgets.forEach((w) => {
150 if (w.type === 'checkbox' && localSettings[w.name] !== undefined) {
151 currentSettings[w.name] = localSettings[w.name];
152 }
153 });
154 onSave(currentConfig.key, currentSettings);
155 };
156
157 const activeCount = useMemo(() => {
158 return widgets.filter(
159 (w) =>
160 w.type === 'checkbox' &&
161 isWidgetEffectivelyOn(w, localSettings[w.name], isPro)
162 ).length;
163 }, [widgets, localSettings, isPro]);
164
165 const cardBase =
166 'flex flex-col justify-between gap-3 rounded-lg border border-solid border-gray-200 p-4';
167
168 const toolbarControl = 'h-9 min-h-9 py-0 text-[13px] leading-none';
169 const toolbarBtn =
170 'inline-flex h-9 min-h-9 shrink-0 cursor-pointer items-center gap-1.5 rounded-md border px-3 py-0 text-[13px] font-semibold leading-none no-underline transition-all duration-200';
171
172 return (
173 <div>
174 <div className="mb-5 flex flex-wrap items-center justify-between gap-2">
175 <h2 className="m-0 text-xl font-bold text-slate-800">
176 {currentConfig.title}
177 </h2>
178 <span className="rounded-2xl border border-slate-200 bg-white px-3 py-1 text-[13px] text-slate-500">
179 {activeCount} / {widgets.filter((w) => w.type === 'checkbox').length}{' '}
180 {__('Active', 'ultimate-store-kit')}
181 </span>
182 </div>
183
184 <div className="mb-5 flex flex-wrap items-center justify-between gap-3 rounded-lg border border-solid border-gray-100 bg-white px-4 py-3">
185 {/* Filters group — wraps on small screens */}
186 <div className="flex flex-wrap items-center gap-3">
187 <div className="relative">
188 <SearchIcon className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 block text-gray-400" />
189 <input
190 type="text"
191 className={`${toolbarControl} block w-full rounded-md border border-solid border-gray-200 bg-transparent pr-10 pl-3 text-slate-700 placeholder:text-gray-500 focus:border-uks-brand focus:outline-none focus:ring-2 focus:ring-uks-brand/25 sm:w-48`}
192 placeholder={__('Search widgets...', 'ultimate-store-kit')}
193 value={search}
194 onChange={(e) => setSearch(e.target.value)}
195 />
196 </div>
197
198 <div className="flex items-center gap-1.5">
199 <label className={selectLabel}>
200 {__('Status:', 'ultimate-store-kit')}
201 </label>
202 <select
203 className={`${selectInput} ${toolbarControl}`}
204 value={filter}
205 onChange={(e) => setFilter(e.target.value)}
206 >
207 <option value="all">{__('All', 'ultimate-store-kit')}</option>
208 <option value="free">{__('Free', 'ultimate-store-kit')}</option>
209 <option value="pro">{__('Pro', 'ultimate-store-kit')}</option>
210 </select>
211 </div>
212
213 {contentTypes.length > 0 && (
214 <div className="flex items-center gap-1.5">
215 <label className={selectLabel}>
216 {__('Template:', 'ultimate-store-kit')}
217 </label>
218 <select
219 className={`${selectInput} ${toolbarControl}`}
220 value={contentTypeFilter}
221 onChange={(e) => setContentTypeFilter(e.target.value)}
222 >
223 <option value="all">{__('All Templates', 'ultimate-store-kit')}</option>
224 {contentTypes.map((type) => (
225 <option key={type} value={type}>
226 {type.charAt(0).toUpperCase() + type.slice(1)}
227 </option>
228 ))}
229 </select>
230 </div>
231 )}
232 </div>
233
234 {/* Action buttons — always pinned to the right, never wrap */}
235 <div className="flex flex-wrap items-center gap-1.5">
236 <button
237 type="button"
238 className={`${toolbarBtn} ${
239 activeCount > 0
240 ? '!bg-uks-brand !text-white hover:!bg-uks-brand-dark border-none'
241 : '!border !border-uks-brand/25 bg-uks-brand/5 text-uks-brand hover:bg-uks-brand/10 hover:text-uks-brand-dark'
242 } focus:outline-none focus:ring-2 focus:ring-uks-brand focus:ring-offset-2 focus:ring-offset-white`}
243 onClick={handleActivateAll}
244 >
245 <CheckIcon className="h-3.5 w-3.5" />
246 {__('Activate All', 'ultimate-store-kit')}
247 </button>
248 <button
249 type="button"
250 className={`${toolbarBtn} ${
251 activeCount === 0
252 ? '!bg-uks-brand !text-white hover:!bg-uks-brand-dark border-none'
253 : '!border !border-uks-brand/25 bg-uks-brand/5 text-uks-brand hover:bg-uks-brand/10 hover:text-uks-brand-dark'
254 } focus:outline-none focus:ring-2 focus:ring-uks-brand focus:ring-offset-2 focus:ring-offset-white`}
255 onClick={handleDeactivateAll}
256 >
257 <CloseIcon className="h-3.5 w-3.5" />
258 {__('Deactivate All', 'ultimate-store-kit')}
259 </button>
260 </div>
261 </div>
262
263 <div className="grid grid-cols-1 gap-3 rounded-lg border border-solid border-gray-100 bg-white p-5 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
264 {filteredWidgets.length === 0 && (
265 <div className="col-span-full py-10 text-center text-sm text-slate-400">
266 {__('No widgets found.', 'ultimate-store-kit')}
267 </div>
268 )}
269 {filteredWidgets.map((widget) => {
270 const isProWidget = widget.widget_type === 'pro';
271 const dependency = widget.dependency || null;
272 const hasMissingDependency =
273 dependency && (!dependency.isInstalled || !dependency.isActive);
274 const isDisabled = (isProWidget && !isPro) || hasMissingDependency;
275 const isActive = isWidgetEffectivelyOn(
276 widget,
277 localSettings[widget.name],
278 isPro
279 );
280
281 return (
282 <div
283 key={widget.name}
284 className={`${cardBase} ${isActive ? 'border-emerald-300' : ''
285 } ${isDisabled ? 'opacity-60 cursor-not-allowed bg-slate-100' : ''}`}
286 >
287 <div className="flex items-start justify-between gap-2">
288 <span className="text-base font-semibold leading-snug text-slate-800">
289 {widget.label}
290 </span>
291 {isProWidget && (
292 <span className={proBadge}>
293 {__('Pro', 'ultimate-store-kit')}
294 </span>
295 )}
296 </div>
297 <div className="flex items-center justify-between">
298 <div className="flex gap-2 items-center">
299 {dependency?.actionUrl && (
300 <a
301
302 href={dependency.actionUrl}
303 target={dependency.actionType === 'install' ? '_blank' : undefined}
304 rel={dependency.actionType === 'install' ? 'noopener noreferrer' : undefined}
305 title={dependency.message || dependency.actionLabel}
306 className="text-gray-400 transition-colors hover:text-uks-brand flex items-center gap-1 decoration-none"
307 style={{
308 textDecoration: 'none',
309 }}
310 >
311 <LinkPluginIcon className="w-4 h-4 block" />
312
313 {__('Install', 'ultimate-store-kit')}
314 </a>
315 )}
316 {widget.demo_url &&
317 !widget.demo_url.startsWith('#') && (
318 <a
319 href={widget.demo_url}
320 target="_blank"
321 rel="noopener noreferrer"
322 title={__(
323 'Demo',
324 'ultimate-store-kit'
325 )}
326 className="text-gray-400 transition-colors hover:text-uks-brand flex items-center gap-1 decoration-none"
327 style={{
328 textDecoration: 'none',
329 }}
330 >
331 <EyeIcon className="w-4 h-4 block" />
332
333 {__('Demo', 'ultimate-store-kit')}
334 </a>
335 )}
336 {widget.video_url && (
337 <a
338 href={widget.video_url}
339 target="_blank"
340 rel="noopener noreferrer"
341 title={__(
342 'Video',
343 'ultimate-store-kit'
344 )}
345 className="text-gray-400 transition-colors hover:text-uks-brand flex items-center gap-1 decoration-none"
346
347 style={{
348 textDecoration: 'none',
349 }}
350 >
351 <PlayIcon className="w-4 h-4 block" />
352
353 {__('Video', 'ultimate-store-kit')}
354 </a>
355 )}
356 </div>
357 <Toggle
358 checked={isActive}
359 disabled={isDisabled}
360 onChange={() =>
361 handleToggle(widget.name)
362 }
363 />
364 </div>
365 {hasMissingDependency && dependency?.actionUrl && (
366 <div className="text-[13px] leading-relaxed text-slate-500">
367 {dependency.message}{' '}
368 <a
369 href={dependency.actionUrl}
370 className="font-semibold text-uks-brand hover:underline"
371 target={dependency.actionType === 'install' ? '_blank' : undefined}
372 rel={dependency.actionType === 'install' ? 'noopener noreferrer' : undefined}
373 >
374 {dependency.actionLabel}
375 </a>
376 </div>
377 )}
378 </div>
379 );
380 })}
381 </div>
382
383 <div className="mt-6 flex justify-end pt-4">
384 <button
385 type="button"
386 className={`${btnLg} ${btnPrimary}`}
387 onClick={handleSave}
388 disabled={saving}
389 >
390 {saving
391 ? __('Saving...', 'ultimate-store-kit')
392 : __('Save Changes', 'ultimate-store-kit')}
393 </button>
394 </div>
395 </div>
396 );
397 };
398
399 export default WidgetsPage;
400