PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.1
3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 2.8.17 All 260 releases
wpfunnels / admin / modules / setup-wizard / components / Content / ProductSync.vue

ProductSync.vue in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.1, at admin/modules/setup-wizard/components/Content/ProductSync.vue

517 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div class="wpfnl-mm-setup">
3 <!-- Heading Section -->
4 <div class="wpfnl-mm-setup-header">
5 <h2 class="wpfnl-mm-setup-title">
6 Product Sync
7 </h2>
8 <p class="wpfnl-mm-setup-subtitle">
9 Get your sales funnel up and running in minutes.
10 </p>
11 </div>
12
13 <!-- product-sync-container -->
14 <div class="wpfnl-product-sync-container">
15 <!-- Search Bar -->
16 <div class="wpfnl-product-sync-search">
17 <div class="wpfnl-product-sync-search-wrapper">
18 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">><path d="M14.0008 14L11.1074 11.1067" stroke="#99a1af" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.33333 12.6667C10.2789 12.6667 12.6667 10.2789 12.6667 7.33333C12.6667 4.38781 10.2789 2 7.33333 2C4.38781 2 2 4.38781 2 7.33333C2 10.2789 4.38781 12.6667 7.33333 12.6667Z" stroke="#99a1af" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/></svg>
19 <input
20 type="text"
21 v-model="searchQuery"
22 @input="handleSearchInput"
23 placeholder="Type to search for a product..."
24 class="wpfnl-product-sync-search-input"
25 />
26 </div>
27
28 <CreateProduct
29 ref="createProduct"
30 :isCreateProduct="isCreateProduct"
31 :isShowModalLoader="isShowModalLoader"
32 @removeCreateProductModal="removeCreateProductModal"
33 />
34 </div>
35
36 <!-- Suggested Product Label -->
37 <div class="wpfnl-product-sync-label">
38 Suggested Product
39 </div>
40
41 <!-- Skeleton Loading -->
42 <div v-if="loading" class="wpfnl-product-sync-skeleton">
43 <div v-for="n in 3" :key="n" class="wpfnl-product-sync-skeleton-card">
44 <div class="wpfnl-skeleton-image wpfnl-skeleton-pulse"></div>
45 <div class="wpfnl-skeleton-content">
46 <div class="wpfnl-skeleton-title wpfnl-skeleton-pulse"></div>
47 <div class="wpfnl-skeleton-description wpfnl-skeleton-pulse"></div>
48 <div class="wpfnl-skeleton-price wpfnl-skeleton-pulse"></div>
49 </div>
50 <div class="wpfnl-skeleton-checkbox wpfnl-skeleton-pulse"></div>
51 </div>
52 </div>
53
54 <!-- Product Cards -->
55 <div v-else-if="visibleProducts.length" class="wpfnl-product-sync-cards">
56 <label
57 v-for="product in visibleProducts"
58 :key="product.id"
59 class="wpfnl-product-sync-card"
60 :class="{ 'active': selectedProduct === product.id }"
61 >
62 <input
63 type="radio"
64 name="product-selection"
65 :value="product.id"
66 v-model.number="selectedProduct"
67 class="wpfnl-product-sync-radio"
68 />
69 <div class="wpfnl-product-image">
70 <img
71 v-if="product.image"
72 :src="product.image"
73 :alt="product.title"
74 loading="lazy"
75 style="width:32px;height:32px;border-radius:6px;object-fit:cover;"
76 />
77 <svg v-else width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">>
78 <path d="M16 2L19.5 12.5H30.5L21.5 19.5L25 30L16 23L7 30L10.5 19.5L1.5 12.5H12.5L16 2Z" fill="#E5E7EB" stroke="#D1D5DB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
79 </svg>
80 </div>
81
82 <div class="wpfnl-product-sync-card-content">
83 <h3 class="wpfnl-product-sync-card-title">{{ product.title }}</h3>
84 <p class="wpfnl-product-sync-card-description">{{ product.description }}</p>
85 <p class="wpfnl-product-sync-card-price">{{ product.price }}</p>
86 </div>
87
88 <div class="wpfnl-product-sync-card-checkbox">
89 <div class="wpfnl-product-sync-checkbox-circle">
90 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">>
91 <path d="M11.6667 3.5L5.25 9.91667L2.33334 7" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
92 </svg>
93 </div>
94 </div>
95 </label>
96 </div>
97
98 <!-- Error State -->
99 <div v-else-if="errorMessage" class="wpfnl-product-sync-state-error">
100 <div class="wpfnl-product-sync-error-icon">
101 <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">>
102 <path d="M14 9.33337V14M14 18.6667H14.0117" stroke="#DC2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
103 <path d="M14 25.6667C20.4434 25.6667 25.6667 20.4434 25.6667 14C25.6667 7.55672 20.4434 2.33337 14 2.33337C7.55672 2.33337 2.33337 7.55672 2.33337 14C2.33337 20.4434 7.55672 25.6667 14 25.6667Z" stroke="#DC2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
104 </svg>
105 </div>
106 <p class="wpfnl-product-sync-error-text">{{ errorMessage }}</p>
107 <button class="wpfnl-product-sync-retry-btn" @click="fetchProducts(searchQuery)">
108 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">>
109 <path d="M1.16663 2.33337V5.83337H4.66663" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
110 <path d="M2.44496 8.75C2.86584 9.89122 3.65241 10.8618 4.68197 11.5098C5.71152 12.1578 6.92457 12.4463 8.13396 12.3316C9.34336 12.2169 10.4816 11.7053 11.3725 10.8741C12.2633 10.0429 12.8544 8.94008 13.054 7.74004C13.2536 6.54 13.0506 5.30844 12.4766 4.23654C11.9027 3.16465 10.9903 2.31163 9.88294 1.81089C8.77557 1.31015 7.53476 1.19003 6.35363 1.46916C5.17249 1.7483 4.11847 2.41065 3.35996 3.35337L1.16663 5.83337" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
111 </svg>
112 Try Again
113 </button>
114 </div>
115
116 <!-- Empty State -->
117 <div v-else class="wpfnl-product-sync-state-empty">
118 <div class="wpfnl-product-sync-empty-icon">
119 <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">>
120 <path d="M33.3333 10L20 3.33337L6.66663 10V30L20 36.6667L33.3333 30V10Z" stroke="#D1D5DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
121 <path d="M20 36.6667V20" stroke="#D1D5DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
122 <path d="M33.3333 10L20 20" stroke="#D1D5DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
123 <path d="M20 20L6.66663 10" stroke="#D1D5DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
124 </svg>
125 </div>
126 <p class="wpfnl-product-sync-empty-title">No products found</p>
127 <p class="wpfnl-product-sync-empty-subtitle">Create your first product to get started with your funnel.</p>
128 <button class="wpfnl-product-sync-create-btn" @click="triggerCreateProduct">
129 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">>
130 <path d="M7 2.91663V11.0833" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
131 <path d="M2.91663 7H11.0833" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
132 </svg>
133 Create Product
134 </button>
135 </div>
136
137 <div class="wpfnl-product-sync-state-error wpfnl-product-sync-template-error" v-if="templateError">
138 <div class="wpfnl-product-sync-error-icon">
139 <svg width="20" height="20" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">>
140 <path d="M14 9.33337V14M14 18.6667H14.0117" stroke="#DC2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
141 <path d="M14 25.6667C20.4434 25.6667 25.6667 20.4434 25.6667 14C25.6667 7.55672 20.4434 2.33337 14 2.33337C7.55672 2.33337 2.33337 7.55672 2.33337 14C2.33337 20.4434 7.55672 25.6667 14 25.6667Z" stroke="#DC2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
142 </svg>
143 </div>
144 <p class="wpfnl-product-sync-error-text">{{ templateError }}</p>
145 </div>
146 </div>
147
148 <!-- Buttons -->
149 <div class="wpfnl-mm-choose-goal-buttons">
150 <button class="wpfnl-mm-btn wpfnl-mm-btn-secondary" @click="goBack">
151 Back
152 </button>
153
154 <div class="wpfnl-mm-btn-group">
155 <button
156 class="wpfnl-mm-btn wpfnl-mm-btn-primary"
157 @click="handleContinue"
158 :disabled="loading || templateLoading || !selectedProduct || !selectedTemplate"
159 >
160 Continue
161 <svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">>
162 <path d="M1 6H16M16 6L11 1M16 6L11 11" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
163 </svg>
164 </button>
165 </div>
166 </div>
167
168 </div>
169 </template>
170
171 <script>
172 import apiFetch from '@wordpress/api-fetch';
173 import { addQueryArgs } from '@wordpress/url';
174 import CreateProduct from './CreateProduct.vue';
175
176 export default {
177 name: 'ProductSync',
178 components: {
179 CreateProduct
180 },
181 isCreateProduct: false,
182 isShowModalLoader: false,
183 productUrl: window?.setup_wizard_obj?.product_url,
184 props: {
185 goal: {
186 type: String,
187 default: 'sales'
188 },
189 builder: {
190 type: String,
191 default: ''
192 },
193 selectedProductId: {
194 type: [Number, String],
195 default: null
196 }
197 },
198 data() {
199 return {
200 selectedProduct: this.normalizeProductId(this.selectedProductId),
201 searchQuery: '',
202 products: [],
203 loading: false,
204 errorMessage: '',
205 perPage: 3,
206 searchPerPage: 10,
207 debounceHandle: null,
208 isCreateProduct: false,
209 isShowModalLoader: false,
210 productUrl: window && window.setup_wizard_obj ? window.setup_wizard_obj.product_url : '',
211 templates: [],
212 selectedTemplate: null,
213 templateLoading: false,
214 templateError: ''
215 };
216 },
217 computed: {
218 visibleProducts() {
219 return this.products;
220 }
221 },
222 watch: {
223 selectedProductId(newValue) {
224 this.selectedProduct = this.normalizeProductId(newValue);
225 },
226 goal: {
227 handler() {
228 this.fetchTemplates();
229 },
230 immediate: false
231 },
232 builder: {
233 handler() {
234 this.fetchTemplates();
235 },
236 immediate: false
237 }
238 },
239 mounted() {
240 this.fetchProducts();
241 this.fetchTemplates();
242 },
243 beforeUnmount() {
244 this.clearDebounce();
245 },
246 methods: {
247 normalizeProductId(value) {
248 if (value === null || value === undefined || value === '') {
249 return null;
250 }
251 const parsed = parseInt(value, 10);
252 return Number.isNaN(parsed) ? null : parsed;
253 },
254 clearDebounce() {
255 if (this.debounceHandle) {
256 clearTimeout(this.debounceHandle);
257 this.debounceHandle = null;
258 }
259 },
260 getTemplateType() {
261 return this.goal === 'sales' ? 'wc' : 'lead';
262 },
263 normalizeBuilderParam(builder) {
264 if (!builder) {
265 return '';
266 }
267 if (builder === 'divi') {
268 return 'divi-builder';
269 }
270 return builder;
271 },
272 shouldFallbackToGutenberg(builderParam) {
273 return builderParam === 'bricks' || builderParam === 'oxygen';
274 },
275 requestTemplates(builderParam, typeParam) {
276 const path = addQueryArgs(
277 `${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/templates/get_templates`,
278 {
279 builder: builderParam,
280 type: typeParam
281 }
282 );
283
284 return apiFetch({ path }).then(response => {
285 if (response && response.success && Array.isArray(response.templates) && response.templates.length) {
286 this.templates = response.templates;
287 const rotationIndex = this.getNextTemplateIndex(response.templates.length, builderParam, typeParam);
288 this.selectedTemplate = response.templates[rotationIndex];
289 return true;
290 }
291 const message = response && response.message ? response.message : 'No templates found for the selected goal.';
292 throw new Error(message);
293 });
294 },
295 getRotationStorageKey(builderParam, typeParam) {
296 const goalKey = typeParam === 'wc' ? 'sales' : typeParam || 'lead';
297 const builderKey = builderParam || 'gutenberg';
298 return `wpfnl_template_rotation_${goalKey}_${builderKey}`;
299 },
300 getNextTemplateIndex(templateCount, builderParam, typeParam) {
301 if (!templateCount) {
302 return 0;
303 }
304 const key = this.getRotationStorageKey(builderParam, typeParam);
305 try {
306 const storedIndex = window?.sessionStorage ? parseInt(window.sessionStorage.getItem(key), 10) : NaN;
307 const nextIndex = Number.isInteger(storedIndex) ? (storedIndex + 1) % templateCount : 0;
308 if (window?.sessionStorage) {
309 window.sessionStorage.setItem(key, nextIndex);
310 }
311 return nextIndex;
312 } catch (error) {
313 return 0;
314 }
315 },
316 fetchTemplates() {
317 this.templateLoading = true;
318 this.templateError = '';
319
320 const builderValue = this.builder ? this.builder : 'gutenberg';
321 const builderParam = this.normalizeBuilderParam(builderValue);
322 const typeParam = this.getTemplateType();
323
324 return this.requestTemplates(builderParam, typeParam)
325 .catch(error => {
326 if (this.shouldFallbackToGutenberg(builderParam)) {
327 return this.requestTemplates('gutenberg', typeParam);
328 }
329 throw error;
330 })
331 .catch(error => {
332 this.templates = [];
333 this.selectedTemplate = null;
334 this.templateError = error && error.message ? error.message : 'Unable to retrieve templates.';
335 })
336 .finally(() => {
337 this.templateLoading = false;
338 });
339 },
340 triggerCreateProduct() {
341 if (this.$refs.createProduct && this.$refs.createProduct.showCreateProductModal) {
342 this.$refs.createProduct.showCreateProductModal();
343 }
344 },
345 handleSearchInput() {
346 this.clearDebounce();
347 this.loading = true;
348 this.errorMessage = '';
349 this.debounceHandle = setTimeout(() => {
350 this.fetchProducts(this.searchQuery);
351 }, 400);
352 },
353 fetchProducts(searchTerm = '') {
354 this.loading = true;
355 this.errorMessage = '';
356
357 const trimmedSearch = searchTerm ? searchTerm.trim() : '';
358 const args = { limit: trimmedSearch ? this.searchPerPage : this.perPage };
359 if (trimmedSearch) {
360 args.search = trimmedSearch;
361 }
362
363 const path = addQueryArgs(
364 `${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/setup-wizard/products`,
365 args
366 );
367
368 apiFetch({ path })
369 .then(response => {
370 if (response && response.success === false) {
371 this.products = [];
372 this.selectedProduct = null;
373 this.errorMessage = response.message || 'Unable to load products.';
374 return;
375 }
376
377 this.products = Array.isArray(response && response.products)
378 ? response.products
379 .filter(product => !!product)
380 .map(product => {
381 const normalizedId = this.normalizeProductId(product.id);
382 if (normalizedId === null) {
383 return null;
384 }
385 return {
386 ...product,
387 id: normalizedId
388 };
389 })
390 .filter(product => !!product)
391 : [];
392
393 if (!this.products.length) {
394 this.selectedProduct = null;
395 return;
396 }
397
398 const hasExistingSelection = this.products.some(
399 product => product.id === this.selectedProduct
400 );
401
402 if (!hasExistingSelection) {
403 this.selectedProduct = this.products[0].id;
404 }
405 })
406 .catch(error => {
407 this.products = [];
408 this.selectedProduct = null;
409 this.errorMessage = error && error.message ? error.message : 'Unable to load products. Please try again.';
410 })
411 .finally(() => {
412 this.loading = false;
413 });
414 },
415 openCreateProductModal() {
416 if (!this.productUrl) {
417 return;
418 }
419
420 this.isCreateProduct = true;
421 this.isShowModalLoader = true;
422
423 this.$nextTick(() => {
424 setTimeout(() => {
425 const container = this.$refs.createProductContainer;
426 if (!container) {
427 this.isShowModalLoader = false;
428 return;
429 }
430
431 container.innerHTML = '';
432
433 const iframe = document.createElement('iframe');
434 iframe.setAttribute('src', this.productUrl);
435 iframe.setAttribute('width', '100%');
436 iframe.setAttribute('height', '100%');
437 iframe.setAttribute('frameborder', '0');
438 iframe.addEventListener('load', () => {
439 this.isShowModalLoader = false;
440 });
441
442 container.appendChild(iframe);
443 }, 100);
444 });
445 },
446 closeCreateProductModal() {
447 this.isCreateProduct = false;
448 this.isShowModalLoader = false;
449 const container = this.$refs.createProductContainer;
450 if (container) {
451 container.innerHTML = '';
452 }
453 },
454 goBack() {
455 this.$emit('prev-step');
456 },
457 handleContinue() {
458 if (!this.selectedProduct) {
459 this.errorMessage = 'Please select a product to continue.';
460 return;
461 }
462
463 if (this.templateLoading) {
464 this.templateError = 'Please wait while we pick the best template for your goal.';
465 return;
466 }
467
468 if (!this.selectedTemplate) {
469 this.templateError = 'Unable to determine a funnel template for this goal. Please try again.';
470 return;
471 }
472
473 this.errorMessage = '';
474 this.templateError = '';
475
476 const selectedProductData = this.products.find(
477 product => product.id === this.selectedProduct
478 ) || null;
479
480 this.$emit('next-step', {
481 productId: this.selectedProduct,
482 product: selectedProductData,
483 template: this.selectedTemplate,
484 });
485 },
486 showCreateProductModal: function () {
487 this.isCreateProduct = true;
488 this.isShowModalLoader = true;
489
490 setTimeout(() => {
491 var container = document.getElementById('wpfnl-create-product');
492
493 // Create an iframe element
494 var iframe = document.createElement('iframe');
495
496 // Set the attributes of the iframe
497 iframe.setAttribute('src', this.productUrl);
498 iframe.setAttribute('width', '100%');
499 iframe.setAttribute('height', '100%');
500 iframe.setAttribute('frameborder', '0');
501
502 iframe.addEventListener('load', () => {
503 this.isShowModalLoader = false;
504 });
505
506 container.appendChild(iframe);
507 // Append the iframe to the container div
508 }, 100);
509 },
510 removeCreateProductModal: function () {
511 this.isCreateProduct = false
512 j('#wpfnl-create-product').empty()
513 },
514 }
515 }
516 </script>
517