PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.10
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.10
3.13.3 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 All 261 releases
wpfunnels / admin / modules / setup-wizard / components / Content / ChooseTemplate.vue

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

344 lines 10.7 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-choose-template">
3 <!-- Header -->
4 <div class="wpfnl-mm-setup-header">
5 <h2 class="wpfnl-mm-setup-title">{{ headerContent.title }}</h2>
6 <p class="wpfnl-mm-setup-subtitle">
7 {{ headerContent.subtitle }}
8 </p>
9 </div>
10
11 <!-- Templates Grid -->
12 <div class="wpfnl-mm-templates-grid" v-if="!loading">
13 <div
14 v-for="template in templates"
15 :key="template.ID"
16 class="wpfnl-mm-template-card"
17 :class="{ 'active': selectedTemplateId === template.ID }"
18 @click="selectTemplate(template)"
19 >
20 <div class="wpfnl-mm-template-card-preview">
21 <img :src="getCardImage(template)" :alt="template.title" v-if="getCardImage(template)">
22 <div class="wpfnl-mm-template-card-overlay">
23 <button class="wpfnl-mm-btn-preview" @click.stop="showPreview(template)">Preview</button>
24 </div>
25 <div class="wpfnl-mm-template-card-checkmark" v-if="selectedTemplateId === template.ID">
26 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">><path d="M0 12C0 5.37258 5.37258 0 12 0C18.6274 0 24 5.37258 24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12Z" fill="#fff"/><path d="M17.3327 8L9.99935 15.3333L6.66602 12" stroke="#6e42d3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
27 </div>
28 </div>
29 <div class="wpfnl-mm-template-card-divider"></div>
30 <div class="wpfnl-mm-template-card-body">
31 <h3 class="wpfnl-mm-template-card-title">{{ template.title }}</h3>
32 <span class="wpfnl-mm-template-card-steps" v-if="template.steps">{{ getDisplayStepCount(template) }} Steps</span>
33 </div>
34 </div>
35 </div>
36
37 <div v-else class="wpfnl-mm-templates-grid wpfnl-mm-templates-skeleton">
38 <div
39 v-for="n in 6"
40 :key="n"
41 class="wpfnl-mm-template-card wpfnl-mm-template-card--skeleton"
42 >
43 <div class="wpfnl-mm-template-card-preview wpfnl-mm-skeleton-block"></div>
44 <div class="wpfnl-mm-template-card-divider"></div>
45 <div class="wpfnl-mm-template-card-body">
46 <div class="wpfnl-mm-skeleton-block wpfnl-mm-skeleton-title"></div>
47 <div class="wpfnl-mm-skeleton-block wpfnl-mm-skeleton-steps"></div>
48 </div>
49 </div>
50 </div>
51
52 <!-- Buttons -->
53 <div class="wpfnl-mm-choose-goal-buttons wpfnl-mm-buttons-container">
54 <button class="wpfnl-mm-btn wpfnl-mm-btn-secondary" @click="goBack">
55 <svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">>
56 <path d="M16 6H1M1 6L6 1M1 6L6 11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
57 </svg>
58 Back
59 </button>
60 <div class="wpfnl-mm-btn-group">
61 <button
62 class="wpfnl-mm-btn wpfnl-mm-btn-primary"
63 @click="handleContinue"
64 :disabled="!selectedTemplateId"
65 >
66 Continue
67 <svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">>
68 <path d="M1 6H16M16 6L11 1M16 6L11 11" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
69 </svg>
70 </button>
71 </div>
72 </div>
73
74 <PreviewTemplate
75 v-if="showPreviewModal"
76 :template="previewTemplate"
77 :isStoreCheckout="isStoreCheckout"
78 :isSalesFunnel="isSalesFunnel"
79 @close="closePreview"
80 @import="importFromPreview"
81 />
82 </div>
83 </template>
84
85 <script>
86 import apiFetch from '@wordpress/api-fetch'
87 import { addQueryArgs } from '@wordpress/url'
88 import PreviewTemplate from './PreviewTemplate.vue'
89
90 export default {
91 name: 'ChooseTemplate',
92 components: {
93 PreviewTemplate
94 },
95 props: {
96 builder: {
97 type: String,
98 required: true
99 },
100 goal: {
101 type: String,
102 default: 'sales'
103 },
104 prefetchedTemplates: {
105 type: Object,
106 default: () => ({})
107 }
108 },
109 data() {
110 return {
111 loading: true,
112 templates: [],
113 selectedTemplateId: null,
114 selectedTemplate: null,
115 showPreviewModal: false,
116 previewTemplate: null,
117 previewTab: 'listing'
118 }
119 },
120 computed: {
121 isStoreCheckout() {
122 return this.goal === 'improve-checkout'
123 },
124 isSalesFunnel() {
125 return this.goal === 'sales'
126 },
127 headerContent() {
128 const headers = {
129 'order-value': {
130 title: 'Boost Your Order Value',
131 subtitle: 'Pick a template with upsells and cross-sells to increase your average order value.'
132 },
133 'improve-checkout': {
134 title: 'Optimize Your Checkout Experience',
135 subtitle: 'Choose a streamlined checkout template to reduce cart abandonment.'
136 },
137 'sales': {
138 title: 'Start With a High-Converting Template',
139 subtitle: 'Pick a ready-made funnel and customize it to match your store.'
140 }
141 };
142 return headers[this.goal] || headers['sales'];
143 }
144 },
145 mounted() {
146 const cached = this.prefetchedTemplates[this.goal];
147 if (cached && cached.length > 0) {
148 this.templates = this.filterTemplates(cached);
149 this.loading = false;
150 } else {
151 this.fetchTemplates();
152 }
153 },
154 watch: {
155 // Handle race condition: component mounted before prefetch completed.
156 prefetchedTemplates(val) {
157 if (!this.loading) return;
158 const cached = val[this.goal];
159 if (cached && cached.length > 0) {
160 this.templates = this.filterTemplates(cached);
161 this.loading = false;
162 }
163 },
164 },
165 methods: {
166 fetchTemplates() {
167 this.loading = true;
168
169 // Map builder ID if necessary (e.g. divi -> divi-builder)
170 let builderParam = this.builder;
171 if (builderParam === 'divi') {
172 builderParam = 'divi-builder';
173 }
174
175 let typeParam = this.goal === 'leads' ? 'lead' : (this.goal === 'improve-checkout' ? 'store_checkout' : 'wc');
176
177 const path = addQueryArgs(`${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/templates/get_templates`, {
178 builder: builderParam,
179 type: typeParam
180 });
181
182 apiFetch({ path: path })
183 .then(response => {
184 if (response.success && response.templates && response.templates.length > 0) {
185 this.templates = this.filterTemplates(response.templates);
186 } else if (builderParam === 'bricks' || builderParam === 'oxygen') {
187 // Fallback to Gutenberg templates if no templates found for Bricks or Oxygen
188 this.fetchGutenbergFallback(typeParam);
189 return;
190 }
191 })
192 .catch(error => {
193 console.error('Error fetching templates:', error);
194 // Also fallback to Gutenberg on error for Bricks/Oxygen
195 if (builderParam === 'bricks' || builderParam === 'oxygen') {
196 this.fetchGutenbergFallback(typeParam);
197 return;
198 }
199 })
200 .finally(() => {
201 this.loading = false;
202 });
203 },
204 fetchGutenbergFallback(typeParam) {
205 const fallbackPath = addQueryArgs(`${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/templates/get_templates`, {
206 builder: 'gutenberg',
207 type: typeParam
208 });
209
210 apiFetch({ path: fallbackPath })
211 .then(response => {
212 if (response.success && response.templates) {
213 this.templates = this.filterTemplates(response.templates);
214 }
215 })
216 .catch(error => {
217 console.error('Error fetching Gutenberg fallback templates:', error);
218 })
219 .finally(() => {
220 this.loading = false;
221 });
222 },
223 filterTemplates(templates) {
224 if (!this.isStoreCheckout && !this.isSalesFunnel) {
225 return templates;
226 }
227
228 // For store checkout, only show templates that have both checkout AND thankyou steps
229 if (this.isStoreCheckout) {
230 return templates
231 .filter(template => template.templateType !== 'pro')
232 .filter(template => {
233 if (!template.steps || !Array.isArray(template.steps)) return false;
234 const stepTypes = template.steps.map(s => s.step_type);
235 return stepTypes.includes('checkout') && stepTypes.includes('thankyou');
236 })
237 .map(template => {
238 // Limit to one checkout + one thankyou step
239 const checkout = template.steps.find(s => s.step_type === 'checkout');
240 const thankyou = template.steps.find(s => s.step_type === 'thankyou');
241 return {
242 ...template,
243 steps: [checkout, thankyou].filter(Boolean)
244 };
245 })
246 .sort((a, b) => a.ID - b.ID);
247 }
248
249 // For sales funnel, only show landing, checkout, and thankyou steps
250 if (this.isSalesFunnel) {
251 return templates
252 .filter(template => {
253 if (!template.steps || !Array.isArray(template.steps)) return false;
254 const stepTypes = template.steps.map(s => s.step_type);
255 // Must have at least checkout and thankyou
256 return stepTypes.includes('checkout') && stepTypes.includes('thankyou');
257 })
258 .map(template => {
259 // Keep only one of each: landing, checkout, and thankyou
260 const landing = template.steps.find(s => s.step_type === 'landing');
261 const checkout = template.steps.find(s => s.step_type === 'checkout');
262 const thankyou = template.steps.find(s => s.step_type === 'thankyou');
263
264 // Return steps in order: landing (if exists), checkout, thankyou
265 const filteredSteps = [landing, checkout, thankyou].filter(Boolean);
266
267 return {
268 ...template,
269 steps: filteredSteps
270 };
271 });
272 }
273
274 return templates;
275 },
276 getCardImage(template) {
277 if (this.isStoreCheckout && template.steps) {
278 // For store checkout, use the checkout step's featured image
279 const checkoutStep = template.steps.find(s => s.step_type === 'checkout');
280 if (checkoutStep && checkoutStep.featured_image) {
281 return checkoutStep.featured_image;
282 }
283 }
284 return template.featured_image;
285 },
286 selectTemplate(template) {
287 if (!template || !template.ID) {
288 console.error('Invalid template:', template);
289 return;
290 }
291 this.selectedTemplateId = template.ID;
292 this.selectedTemplate = template;
293 },
294 showPreview(template) {
295 this.previewTemplate = template;
296 this.showPreviewModal = true;
297 this.previewTab = 'listing';
298 },
299 closePreview() {
300 this.showPreviewModal = false;
301 this.previewTemplate = null;
302 },
303 importFromPreview(template) {
304 // Close the preview modal
305 this.closePreview();
306
307 // Import the template
308 this.importTemplate(template);
309 },
310 importTemplate(template) {
311 // Select the template
312 this.selectedTemplateId = template.id;
313 this.selectedTemplate = template;
314
315 // Close preview modal if open
316 if (this.showPreviewModal) {
317 this.closePreview();
318 }
319
320 // Immediately go to next step
321 this.$emit('next-step', {
322 template: this.selectedTemplate
323 });
324 },
325 goBack() {
326 this.$emit('prev-step');
327 },
328 getDisplayStepCount(template) {
329 if (!template.steps) return 0;
330 // Since filterTemplates already filters the steps array,
331 // we can just return the length directly
332 return template.steps.length;
333 },
334 handleContinue() {
335 if (!this.selectedTemplateId) return;
336
337 this.$emit('next-step', {
338 template: this.selectedTemplate
339 });
340 }
341 }
342 }
343 </script>
344