| 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 |
|