@ecomplus/storefront-components 1.0.0-beta.17 → 1.0.0-beta.171

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +1224 -131
  2. package/README.md +9 -4
  3. package/all.js +3 -1
  4. package/dist/1.storefront-components.min.js +2 -0
  5. package/dist/1.storefront-components.min.js.map +1 -0
  6. package/dist/2.storefront-components.min.js +5 -0
  7. package/dist/2.storefront-components.min.js.map +1 -0
  8. package/dist/3.storefront-components.min.js +5 -0
  9. package/dist/3.storefront-components.min.js.map +1 -0
  10. package/dist/storefront-components.min.js +33 -12
  11. package/dist/storefront-components.min.js.map +1 -1
  12. package/package.json +19 -14
  13. package/src/APagination.vue +2 -0
  14. package/src/AShare.vue +2 -0
  15. package/src/AccountAddresses.vue +3 -0
  16. package/src/AccountForm.vue +3 -0
  17. package/src/AccountPoints.vue +3 -0
  18. package/src/BuyTogether.vue +3 -0
  19. package/src/EarnPointsProgress.vue +3 -0
  20. package/src/ItemCustomizations.vue +2 -0
  21. package/src/KitProductVariations.vue +3 -0
  22. package/src/PointsApplier.vue +2 -0
  23. package/src/ProductQuickview.vue +3 -0
  24. package/src/QuantitySelector.vue +3 -0
  25. package/src/RecommendedItems.vue +3 -0
  26. package/src/ShippingLine.vue +1 -0
  27. package/src/TheCart.vue +3 -0
  28. package/src/html/APagination.html +90 -0
  29. package/src/html/APrices.html +24 -4
  30. package/src/html/AShare.html +31 -0
  31. package/src/html/AccountAddresses.html +90 -0
  32. package/src/html/AccountForm.html +269 -0
  33. package/src/html/AccountPoints.html +39 -0
  34. package/src/html/AddressForm.html +9 -7
  35. package/src/html/BuyTogether.html +60 -0
  36. package/src/html/CartItem.html +86 -38
  37. package/src/html/CartQuickview.html +28 -5
  38. package/src/html/DiscountApplier.html +2 -2
  39. package/src/html/EarnPointsProgress.html +28 -0
  40. package/src/html/InputDate.html +1 -1
  41. package/src/html/InputDocNumber.html +1 -0
  42. package/src/html/InputPhone.html +1 -1
  43. package/src/html/InstantSearch.html +3 -3
  44. package/src/html/ItemCustomizations.html +14 -0
  45. package/src/html/KitProductVariations.html +92 -0
  46. package/src/html/LoginBlock.html +34 -32
  47. package/src/html/LoginModal.html +9 -4
  48. package/src/html/PaymentOption.html +7 -5
  49. package/src/html/PointsApplier.html +26 -0
  50. package/src/html/ProductCard.html +56 -8
  51. package/src/html/ProductGallery.html +21 -3
  52. package/src/html/ProductQuickview.html +64 -0
  53. package/src/html/ProductVariations.html +30 -3
  54. package/src/html/QuantitySelector.html +85 -0
  55. package/src/html/RecommendedItems.html +48 -0
  56. package/src/html/SearchEngine.html +100 -24
  57. package/src/html/ShippingCalculator.html +53 -3
  58. package/src/html/ShippingLine.html +5 -2
  59. package/src/html/TheAccount.html +43 -9
  60. package/src/html/TheCart.html +156 -0
  61. package/src/html/TheProduct.html +416 -138
  62. package/src/js/APagination.js +74 -0
  63. package/src/js/APicture.js +27 -7
  64. package/src/js/APrices.js +80 -41
  65. package/src/js/AShare.js +83 -0
  66. package/src/js/AccountAddresses.js +192 -0
  67. package/src/js/AccountForm.js +312 -0
  68. package/src/js/AccountPoints.js +63 -0
  69. package/src/js/AddressForm.js +80 -35
  70. package/src/js/BuyTogether.js +246 -0
  71. package/src/js/CartItem.js +67 -14
  72. package/src/js/CartQuickview.js +21 -2
  73. package/src/js/DiscountApplier.js +132 -42
  74. package/src/js/EarnPointsProgress.js +77 -0
  75. package/src/js/InputDate.js +8 -8
  76. package/src/js/InputDocNumber.js +20 -0
  77. package/src/js/ItemCustomizations.js +10 -0
  78. package/src/js/KitProductVariations.js +218 -0
  79. package/src/js/LoginBlock.js +14 -2
  80. package/src/js/LoginModal.js +17 -4
  81. package/src/js/PaymentOption.js +28 -1
  82. package/src/js/PointsApplier.js +110 -0
  83. package/src/js/ProductCard.js +97 -11
  84. package/src/js/ProductGallery.js +32 -12
  85. package/src/js/ProductQuickview.js +72 -0
  86. package/src/js/ProductVariations.js +76 -19
  87. package/src/js/QuantitySelector.js +175 -0
  88. package/src/js/RecommendedItems.js +178 -0
  89. package/src/js/SearchEngine.js +185 -55
  90. package/src/js/ShippingCalculator.js +157 -33
  91. package/src/js/ShippingLine.js +21 -5
  92. package/src/js/TheAccount.js +87 -6
  93. package/src/js/TheCart.js +146 -0
  94. package/src/js/TheProduct.js +387 -43
  95. package/src/js/helpers/add-idle-callback.js +7 -0
  96. package/src/js/helpers/check-form-validity.js +3 -0
  97. package/src/js/helpers/favorite-products.js +24 -0
  98. package/src/js/helpers/scroll-to-element.js +10 -0
  99. package/src/js/helpers/sort-apps.js +14 -0
  100. package/src/js/helpers/wait-storefront-info.js +21 -0
  101. package/src/scss/APicture.scss +2 -0
  102. package/src/scss/APrices.scss +13 -1
  103. package/src/scss/AccountAddresses.scss +27 -0
  104. package/src/scss/AccountForm.scss +5 -0
  105. package/src/scss/AccountPoints.scss +17 -0
  106. package/src/scss/BuyTogether.scss +38 -0
  107. package/src/scss/CartItem.scss +17 -1
  108. package/src/scss/EarnPointsProgress.scss +6 -0
  109. package/src/scss/InstantSearch.scss +1 -0
  110. package/src/scss/KitProductVariations.scss +72 -0
  111. package/src/scss/LoginBlock.scss +5 -0
  112. package/src/scss/PaymentOption.scss +10 -1
  113. package/src/scss/ProductCard.scss +66 -28
  114. package/src/scss/ProductGallery.scss +4 -2
  115. package/src/scss/ProductQuickview.scss +36 -0
  116. package/src/scss/ProductVariations.scss +20 -4
  117. package/src/scss/QuantitySelector.scss +39 -0
  118. package/src/scss/RecommendedItems.scss +28 -0
  119. package/src/scss/SearchEngine.scss +9 -5
  120. package/src/scss/ShippingCalculator.scss +30 -1
  121. package/src/scss/ShippingLine.scss +20 -0
  122. package/src/scss/TheAccount.scss +4 -0
  123. package/src/scss/TheCart.scss +54 -0
  124. package/src/scss/TheProduct.scss +146 -1
  125. package/webpack.config.js +20 -6
@@ -1,4 +1,6 @@
1
1
  import {
2
+ i19all,
3
+ i19asOf,
2
4
  i19brands,
3
5
  i19categories,
4
6
  i19clearFilters,
@@ -9,21 +11,31 @@ import {
9
11
  i19highestPrice,
10
12
  i19itemsFound,
11
13
  i19lowestPrice,
14
+ i19name,
12
15
  i19noResultsFor,
13
16
  i19popularProducts,
17
+ i19price,
14
18
  i19refineSearch,
19
+ i19releases,
15
20
  i19relevance,
16
21
  i19results,
17
22
  i19sales,
18
23
  i19searchAgain,
19
24
  i19searchingFor,
20
25
  i19searchOfflineErrorMsg,
21
- i19sort
26
+ i19sort,
27
+ i19upTo
22
28
  } from '@ecomplus/i18n'
23
29
 
24
- import { i18n } from '@ecomplus/utils'
30
+ import {
31
+ i18n,
32
+ formatMoney
33
+ } from '@ecomplus/utils'
34
+
25
35
  import lozad from 'lozad'
26
36
  import EcomSearch from '@ecomplus/search-engine'
37
+ import { Portal } from '@linusborg/vue-simple-portal'
38
+ import scrollToElement from './helpers/scroll-to-element'
27
39
  import ABackdrop from '../ABackdrop.vue'
28
40
  import ProductCard from '../ProductCard.vue'
29
41
 
@@ -44,6 +56,7 @@ export default {
44
56
  name: 'SearchEngine',
45
57
 
46
58
  components: {
59
+ Portal,
47
60
  ABackdrop,
48
61
  ProductCard
49
62
  },
@@ -63,6 +76,7 @@ export default {
63
76
  isFixedBrands: Boolean,
64
77
  isFixedCategories: Boolean,
65
78
  defaultSort: String,
79
+ defaultFilters: Object,
66
80
  autoFixScore: {
67
81
  type: Number,
68
82
  default: 0.6
@@ -79,10 +93,15 @@ export default {
79
93
  type: Boolean,
80
94
  default: true
81
95
  },
96
+ loadMoreSelector: String,
82
97
  canRetry: {
83
98
  type: Boolean,
84
99
  default: true
85
100
  },
101
+ canShowItems: {
102
+ type: Boolean,
103
+ default: true
104
+ },
86
105
  productCardProps: Object,
87
106
  gridsData: {
88
107
  type: Array,
@@ -103,6 +122,9 @@ export default {
103
122
  noResultsTerm: '',
104
123
  keepNoResultsTerm: false,
105
124
  filters: [],
125
+ priceRange: {},
126
+ priceOptions: [],
127
+ hasSetPriceRange: false,
106
128
  lastSelectedFilter: null,
107
129
  selectedOptions: {},
108
130
  selectedSortOption: null,
@@ -110,6 +132,7 @@ export default {
110
132
  lastRequestId: null,
111
133
  isScheduled: false,
112
134
  isLoadingMore: false,
135
+ mustSkipLoadMore: false,
113
136
  hasNetworkError: false,
114
137
  popularItems: [],
115
138
  hasSetPopularItems: false,
@@ -119,6 +142,7 @@ export default {
119
142
  },
120
143
 
121
144
  computed: {
145
+ i19all: () => i18n(i19all),
122
146
  i19clearFilters: () => i18n(i19clearFilters),
123
147
  i19closeFilters: () => i18n(i19closeFilters),
124
148
  i19didYouMean: () => i18n(i19didYouMean),
@@ -127,8 +151,9 @@ export default {
127
151
  i19itemsFound: () => i18n(i19itemsFound),
128
152
  i19noResultsFor: () => i18n(i19noResultsFor),
129
153
  i19popularProducts: () => i18n(i19popularProducts),
130
- i19relevance: () => i18n(i19relevance),
154
+ i19price: () => i18n(i19price),
131
155
  i19refineSearch: () => i18n(i19refineSearch),
156
+ i19relevance: () => i18n(i19relevance),
132
157
  i19results: () => i18n(i19results),
133
158
  i19searchAgain: () => i18n(i19searchAgain),
134
159
  i19searchingFor: () => i18n(i19searchingFor),
@@ -158,6 +183,12 @@ export default {
158
183
  }, {
159
184
  value: 'highest_price',
160
185
  label: i18n(i19highestPrice)
186
+ }, {
187
+ value: 'news',
188
+ label: i18n(i19releases)
189
+ }, {
190
+ value: 'slug',
191
+ label: i18n(i19name)
161
192
  }
162
193
  ],
163
194
 
@@ -172,7 +203,10 @@ export default {
172
203
 
173
204
  isNavVisible () {
174
205
  return this.hasSearched && this.isFilterable &&
175
- (this.isSearching || this.totalSearchResults > 8 || this.hasSelectedOptions)
206
+ (this.isSearching ||
207
+ this.totalSearchResults > 8 ||
208
+ this.hasSelectedOptions ||
209
+ this.hasSetPriceRange)
176
210
  },
177
211
 
178
212
  isResultsVisible () {
@@ -181,7 +215,8 @@ export default {
181
215
 
182
216
  hasFilters () {
183
217
  return this.hasSelectedOptions ||
184
- this.filters.find(({ options }) => options.length)
218
+ this.filters.find(({ options }) => options.length) ||
219
+ this.hasSetPriceRange
185
220
  },
186
221
 
187
222
  suggestedItems () {
@@ -191,10 +226,18 @@ export default {
191
226
  loadObserver () {
192
227
  return this.canLoadMore && lozad('#search-engine-load-more', {
193
228
  load: () => {
194
- this.isLoadingMore = true
195
- this.fetchItems()
229
+ if (!this.mustSkipLoadMore) {
230
+ this.mustSkipLoadMore = this.isLoadingMore = true
231
+ this.fetchItems()
232
+ }
196
233
  }
197
234
  })
235
+ },
236
+
237
+ pageAnchorIndex () {
238
+ const count = this.suggestedItems.length
239
+ const rest = count % this.pageSize
240
+ return (rest === 0 ? count - this.pageSize : count - rest) - 1
198
241
  }
199
242
  },
200
243
 
@@ -208,7 +251,7 @@ export default {
208
251
  ecomSearch.setPageNumber(this.page + Math.ceil(this.resultItems.length / this.pageSize))
209
252
  }
210
253
  const fetching = ecomSearch.setPageSize(this.pageSize).fetch()
211
- .then(() => {
254
+ .then(result => {
212
255
  if (this.lastRequestId === requestId) {
213
256
  this.hasNetworkError = false
214
257
  if (!isPopularItems) {
@@ -219,6 +262,7 @@ export default {
219
262
  this.hasSetPopularItems = true
220
263
  this.popularItems = ecomSearch.getItems()
221
264
  }
265
+ return result
222
266
  })
223
267
  .catch(err => {
224
268
  console.error(err)
@@ -232,9 +276,15 @@ export default {
232
276
  })
233
277
  .finally(() => {
234
278
  this.countOpenRequests--
235
- this.isLoadingMore = false
279
+ if (this.isLoadingMore) {
280
+ this.isLoadingMore = false
281
+ this.$nextTick(() => setTimeout(() => {
282
+ this.mustSkipLoadMore = false
283
+ this.loadObserver.observe()
284
+ }, 300))
285
+ }
236
286
  })
237
- this.$emit('fetch', { ecomSearch, fetching })
287
+ this.$emit('fetch', { ecomSearch, fetching, isPopularItems })
238
288
  },
239
289
 
240
290
  updateFilters () {
@@ -256,7 +306,8 @@ export default {
256
306
  options,
257
307
  isSpec
258
308
  }
259
- const optionsList = !this.selectedOptions[filter] ? []
309
+ const optionsList = !this.selectedOptions[filter]
310
+ ? []
260
311
  : this.selectedOptions[filter]
261
312
  .filter(option => options.find(({ key }) => key === option))
262
313
  this.$set(this.selectedOptions, filter, optionsList)
@@ -265,46 +316,75 @@ export default {
265
316
  }
266
317
  addFilter('Brands', this.ecomSearch.getBrands())
267
318
  addFilter('Categories', this.ecomSearch.getCategories())
268
- this.ecomSearch.getSpecs().forEach(({ key, options }, index) => {
319
+ this.ecomSearch.getSpecs().forEach(({ key, options }) => {
269
320
  addFilter(key, options, true)
270
321
  })
271
322
  this.filters = this.filters.filter((_, i) => updatedFilters.includes(i))
272
323
  this.searchFilterId = Date.now()
273
324
  },
274
325
 
275
- handleSuggestions () {
276
- const { ecomSearch, term } = this
277
- let suggestTerm = term
278
- let canAutoFix = false
279
- this.suggestedTerm = ''
280
- ecomSearch.getTermSuggestions().forEach(({ options, text }) => {
281
- if (options.length) {
282
- const opt = options[0]
283
- if (
284
- !this.totalSearchResults &&
285
- this.autoFixScore > 0 &&
286
- opt.score >= this.autoFixScore &&
287
- opt.text.indexOf(term) === -1
288
- ) {
289
- canAutoFix = true
290
- }
291
- suggestTerm = suggestTerm.replace(text, opt.text)
326
+ updatePriceOptions () {
327
+ this.priceRange = this.ecomSearch.getPriceRange()
328
+ if (Math.round(this.priceRange.min) < Math.round(this.priceRange.avg)) {
329
+ const price1 = Math.ceil(Math.max(this.priceRange.min * 1.5, this.priceRange.avg / 2))
330
+ const price2 = Math.ceil(Math.min(this.priceRange.max / 1.5, this.priceRange.avg * 2))
331
+ if (price1 !== price2) {
332
+ this.priceOptions = [Math.min(price1, price2), Math.max(price1, price2), null]
333
+ .map((max, i, prices) => {
334
+ const min = prices[i - 1]
335
+ return {
336
+ min,
337
+ max,
338
+ label: !min
339
+ ? `${i18n(i19upTo)} ${formatMoney(max)}`
340
+ : i < 2
341
+ ? `${formatMoney(min)} - ${formatMoney(max)}`
342
+ : `${i18n(i19asOf)} ${formatMoney(min)}`
343
+ }
344
+ })
345
+ return
292
346
  }
293
- })
294
- if (!this.keepNoResultsTerm) {
295
- this.noResultsTerm = ''
296
- } else {
297
- this.keepNoResultsTerm = false
298
347
  }
299
- if (suggestTerm !== term) {
300
- if (canAutoFix) {
301
- this.noResultsTerm = term
302
- this.keepNoResultsTerm = true
303
- this.$emit('update:term', suggestTerm)
348
+ this.priceOptions = []
349
+ },
350
+
351
+ handleSuggestions () {
352
+ if (this.term) {
353
+ const { ecomSearch } = this
354
+ const term = this.term.toLowerCase()
355
+ let suggestTerm = term
356
+ let canAutoFix = false
357
+ this.suggestedTerm = ''
358
+ ecomSearch.getTermSuggestions().forEach(({ options, text }) => {
359
+ if (options.length) {
360
+ const opt = options[0]
361
+ const optTerm = opt.text.toLowerCase()
362
+ if (
363
+ !this.totalSearchResults &&
364
+ this.autoFixScore > 0 &&
365
+ opt.score >= this.autoFixScore &&
366
+ optTerm.indexOf(term) === -1
367
+ ) {
368
+ canAutoFix = true
369
+ }
370
+ suggestTerm = suggestTerm.replace(new RegExp(text, 'i'), optTerm)
371
+ }
372
+ })
373
+ if (!this.keepNoResultsTerm) {
374
+ this.noResultsTerm = ''
304
375
  } else {
305
- this.suggestedTerm = suggestTerm
376
+ this.keepNoResultsTerm = false
377
+ }
378
+ if (suggestTerm !== term) {
379
+ if (canAutoFix) {
380
+ this.noResultsTerm = term
381
+ this.keepNoResultsTerm = true
382
+ this.$emit('update:term', suggestTerm)
383
+ } else {
384
+ this.suggestedTerm = suggestTerm
385
+ }
386
+ ecomSearch.history.shift()
306
387
  }
307
- ecomSearch.history.shift()
308
388
  }
309
389
  },
310
390
 
@@ -315,6 +395,17 @@ export default {
315
395
  ? this.resultItems.concat(ecomSearch.getItems())
316
396
  : ecomSearch.getItems()
317
397
  this.updateFilters()
398
+ if (!this.hasSearched && this.defaultFilters) {
399
+ for (const filter in this.defaultFilters) {
400
+ const options = this.defaultFilters[filter]
401
+ if (Array.isArray(options)) {
402
+ options.forEach(option => this.setFilterOption(filter, option, true))
403
+ } else if (typeof options === 'string') {
404
+ this.setFilterOption(filter, options, true)
405
+ }
406
+ }
407
+ }
408
+ this.updatePriceOptions()
318
409
  this.handleSuggestions()
319
410
  if (!this.totalSearchResults && this.hasPopularItems && !this.hasSetPopularItems) {
320
411
  this.fetchItems(false, true)
@@ -349,7 +440,8 @@ export default {
349
440
 
350
441
  toggleFilters (isVisible) {
351
442
  this.isAsideVisible = typeof isVisible === 'boolean'
352
- ? isVisible : !this.isAsideVisible
443
+ ? isVisible
444
+ : !this.isAsideVisible
353
445
  },
354
446
 
355
447
  getFilterLabel (filter) {
@@ -409,23 +501,53 @@ export default {
409
501
  }
410
502
  },
411
503
 
504
+ handlePriceInputs () {
505
+ const { inputMinPrice, inputMaxPrice } = this.$refs
506
+ const min = Number(inputMinPrice.value) || null
507
+ const max = Number(inputMaxPrice.value) || null
508
+ if ((min && !max) || min <= max) {
509
+ this.setPriceRange(min, max)
510
+ }
511
+ inputMinPrice.value = (min || '')
512
+ inputMaxPrice.value = (max || '')
513
+ },
514
+
515
+ setPriceRange (min, max) {
516
+ if (
517
+ (min && min !== this.priceRange.min) ||
518
+ (max && max !== this.priceRange.max)
519
+ ) {
520
+ this.hasSetPriceRange = true
521
+ } else if (this.hasSetPriceRange) {
522
+ this.hasSetPriceRange = false
523
+ } else {
524
+ return
525
+ }
526
+ this.ecomSearch.setPriceRange(min, max)
527
+ this.scheduleFetch()
528
+ },
529
+
412
530
  setFilterOption (filter, option, isSet) {
413
531
  const { selectedOptions } = this
414
532
  const optionsList = selectedOptions[filter]
415
- if (isSet) {
416
- this.lastSelectedFilter = filter
417
- optionsList.push(option)
418
- } else {
533
+ if (optionsList) {
419
534
  const optionIndex = optionsList.indexOf(option)
420
- if (optionIndex > -1) {
421
- optionsList.splice(optionIndex, 1)
422
- }
423
- if (!optionsList.length && this.lastSelectedFilter === filter) {
424
- this.lastSelectedFilter = null
535
+ if (isSet) {
536
+ if (optionIndex === -1) {
537
+ this.lastSelectedFilter = filter
538
+ optionsList.push(option)
539
+ }
540
+ } else {
541
+ if (optionIndex > -1) {
542
+ optionsList.splice(optionIndex, 1)
543
+ }
544
+ if (!optionsList.length && this.lastSelectedFilter === filter) {
545
+ this.lastSelectedFilter = null
546
+ }
425
547
  }
548
+ this.updateSearchFilter(filter)
549
+ this.scheduleFetch()
426
550
  }
427
- this.updateSearchFilter(filter)
428
- this.scheduleFetch()
429
551
  },
430
552
 
431
553
  clearFilters () {
@@ -442,7 +564,11 @@ export default {
442
564
  setSortOrder (sort) {
443
565
  this.selectedSortOption = sort
444
566
  this.ecomSearch.setSortOrder(sort)
445
- this.scheduleFetch()
567
+ if (this.page > 1) {
568
+ this.page = 1
569
+ } else {
570
+ this.scheduleFetch()
571
+ }
446
572
  }
447
573
  },
448
574
 
@@ -465,7 +591,11 @@ export default {
465
591
  isSearching (isSearching) {
466
592
  if (!isSearching && this.loadObserver) {
467
593
  this.$nextTick(() => {
468
- this.loadObserver.observe()
594
+ if (!this.mustSkipLoadMore) {
595
+ this.loadObserver.observe()
596
+ } else {
597
+ setTimeout(() => scrollToElement(this.$refs.pageAnchor[0], 40), 100)
598
+ }
469
599
  })
470
600
  }
471
601
  }
@@ -1,5 +1,7 @@
1
1
  import {
2
+ i19add$1ToEarn,
2
3
  i19calculateShipping,
4
+ i19freeShipping,
3
5
  i19zipCode
4
6
  } from '@ecomplus/i18n'
5
7
 
@@ -11,6 +13,7 @@ import {
11
13
  } from '@ecomplus/utils'
12
14
 
13
15
  import { modules } from '@ecomplus/client'
16
+ import sortApps from './helpers/sort-apps'
14
17
  import CleaveInput from 'vue-cleave-component'
15
18
  import ShippingLine from '../ShippingLine.vue'
16
19
 
@@ -25,6 +28,7 @@ const reduceItemBody = itemOrProduct => {
25
28
  'sku',
26
29
  'name',
27
30
  'quantity',
31
+ 'inventory',
28
32
  'currency_id',
29
33
  'currency_symbol',
30
34
  'price',
@@ -75,26 +79,62 @@ export default {
75
79
  default () {
76
80
  return {}
77
81
  }
82
+ },
83
+ skipAppIds: Array,
84
+ shippingAppsSort: {
85
+ type: Array,
86
+ default () {
87
+ return window.ecomShippingApps || []
88
+ }
78
89
  }
79
90
  },
80
91
 
81
92
  data () {
82
93
  return {
83
94
  localZipCode: null,
95
+ localShippedItems: [],
96
+ amountSubtotal: null,
84
97
  shippingServices: [],
85
98
  selectedService: null,
86
- isWaiting: false
99
+ hasPaidOption: false,
100
+ freeFromValue: null,
101
+ isScheduled: false,
102
+ retryTimer: null,
103
+ isWaiting: false,
104
+ hasCalculated: false
87
105
  }
88
106
  },
89
107
 
90
108
  computed: {
109
+ i19add$1ToEarn: () => i18n(i19add$1ToEarn),
91
110
  i19calculateShipping: () => i18n(i19calculateShipping),
92
111
  i19zipCode: () => i18n(i19zipCode),
112
+ i19freeShipping: () => i18n(i19freeShipping).toLowerCase(),
93
113
 
94
114
  cleaveOptions () {
95
115
  return this.countryCode === 'BR'
96
116
  ? { blocks: [5, 3], delimiter: '-' }
97
117
  : { blocks: [30] }
118
+ },
119
+
120
+ freeFromPercentage () {
121
+ return this.hasPaidOption && this.amountSubtotal < this.freeFromValue
122
+ ? Math.round(this.amountSubtotal * 100 / this.freeFromValue)
123
+ : null
124
+ },
125
+
126
+ productionDeadline () {
127
+ let maxDeadline = 0
128
+ this.shippedItems.forEach(item => {
129
+ if (item.quantity && item.production_time) {
130
+ const { days, cumulative } = item.production_time
131
+ const itemDeadline = cumulative ? days * item.quantity : days
132
+ if (itemDeadline > maxDeadline) {
133
+ maxDeadline = itemDeadline
134
+ }
135
+ }
136
+ })
137
+ return maxDeadline
98
138
  }
99
139
  },
100
140
 
@@ -105,56 +145,115 @@ export default {
105
145
  this.$emit('update:zip-code', this.localZipCode)
106
146
  },
107
147
 
108
- parseShippingOptions (shippingResult = [], isRetry) {
148
+ parseShippingOptions (shippingResult = [], isRetry = false) {
149
+ this.freeFromValue = null
109
150
  this.shippingServices = []
110
- let canRetry
111
151
  if (shippingResult.length) {
112
152
  shippingResult.forEach(appResult => {
113
153
  const { validated, error, response } = appResult
114
- if (validated && !error) {
115
- response.shipping_services.forEach(service => {
116
- this.shippingServices.push({
117
- app_id: appResult.app_id,
118
- ...service
119
- })
154
+ if (!validated || error) {
155
+ return
156
+ }
157
+ if (
158
+ this.skipAppIds &&
159
+ this.skipAppIds.includes(appResult.app_id) &&
160
+ shippingResult.filter(({ app_id: appId }) => !this.skipAppIds.includes(appId)).length
161
+ ) {
162
+ return
163
+ }
164
+ response.shipping_services.forEach(service => {
165
+ this.shippingServices.push({
166
+ app_id: appResult.app_id,
167
+ ...service
120
168
  })
121
- } else if (isRetry !== true && (!response || !response.error)) {
122
- canRetry = true
169
+ })
170
+ const freeShippingFromValue = response.free_shipping_from_value
171
+ if (
172
+ freeShippingFromValue &&
173
+ (!this.freeFromValue || this.freeFromValue > freeShippingFromValue)
174
+ ) {
175
+ this.freeFromValue = freeShippingFromValue
123
176
  }
124
177
  })
125
178
  if (!this.shippingServices.length) {
126
- if (canRetry) {
179
+ if (!isRetry) {
127
180
  this.fetchShippingServices(true)
181
+ } else {
182
+ this.scheduleRetry()
128
183
  }
129
184
  } else {
185
+ this.shippingServices = this.shippingServices.sort((a, b) => {
186
+ const priceDiff = a.shipping_line.total_price - b.shipping_line.total_price
187
+ return priceDiff < 0
188
+ ? -1
189
+ : priceDiff > 0
190
+ ? 1
191
+ : a.shipping_line.delivery_time && b.shipping_line.delivery_time &&
192
+ a.shipping_line.delivery_time.days < b.shipping_line.delivery_time.days
193
+ ? -1
194
+ : 1
195
+ })
130
196
  this.setSelectedService(0)
197
+ this.hasPaidOption = Boolean(this.shippingServices.find(service => {
198
+ return service.shipping_line.total_price || service.shipping_line.price
199
+ }))
200
+ if (Array.isArray(this.shippingAppsSort) && this.shippingAppsSort.length) {
201
+ this.shippingServices = sortApps(this.shippingServices, this.shippingAppsSort)
202
+ }
131
203
  }
132
204
  }
133
205
  },
134
206
 
135
- fetchShippingServices (isRetry) {
136
- const { storeId } = this
137
- const url = '/calculate_shipping.json'
138
- const method = 'POST'
139
- const data = {
140
- ...this.shippingData,
141
- to: {
142
- zip: this.localZipCode,
143
- ...this.shippingData.to
207
+ scheduleRetry (timeout = 10000) {
208
+ clearTimeout(this.retryTimer)
209
+ this.retryTimer = setTimeout(() => {
210
+ if (this.localZipCode && !this.shippingServices.length && this.shippedItems.length) {
211
+ this.fetchShippingServices(true)
144
212
  }
213
+ }, timeout)
214
+ },
215
+
216
+ fetchShippingServices (isRetry) {
217
+ if (!this.isScheduled) {
218
+ this.isScheduled = true
219
+ setTimeout(() => {
220
+ this.isScheduled = false
221
+ const { storeId } = this
222
+ let url = '/calculate_shipping.json'
223
+ if (this.skipAppIds && this.skipAppIds.length) {
224
+ url += '?skip_ids='
225
+ this.skipAppIds.forEach((appId, i) => {
226
+ if (i > 0) url += ','
227
+ url += `${appId}`
228
+ })
229
+ }
230
+ const method = 'POST'
231
+ const data = {
232
+ ...this.shippingData,
233
+ to: {
234
+ zip: this.localZipCode,
235
+ ...this.shippingData.to
236
+ }
237
+ }
238
+ if (this.localShippedItems.length) {
239
+ data.items = this.localShippedItems
240
+ data.subtotal = this.amountSubtotal
241
+ }
242
+ this.isWaiting = true
243
+ modules({ url, method, storeId, data })
244
+ .then(({ data }) => this.parseShippingOptions(data.result, isRetry))
245
+ .catch(err => {
246
+ if (!isRetry) {
247
+ this.scheduleRetry(4000)
248
+ }
249
+ console.error(err)
250
+ })
251
+ .finally(() => {
252
+ this.hasCalculated = true
253
+ this.isWaiting = false
254
+ })
255
+ }, this.hasCalculated ? 150 : 50)
145
256
  }
146
- if (this.shippedItems.length) {
147
- data.items = this.shippedItems.map(reduceItemBody)
148
- const itemsToSubtotal = (subtotal, item) => subtotal + getPrice(item) * item.quantity
149
- data.subtotal = data.items.reduce(itemsToSubtotal, 0)
150
- }
151
- this.isWaiting = true
152
- modules({ url, method, storeId, data })
153
- .then(({ data }) => this.parseShippingOptions(data.result, isRetry))
154
- .catch(console.error)
155
- .finally(() => {
156
- this.isWaiting = false
157
- })
158
257
  },
159
258
 
160
259
  submitZipCode () {
@@ -174,6 +273,27 @@ export default {
174
273
  },
175
274
 
176
275
  watch: {
276
+ shippedItems: {
277
+ handler () {
278
+ setTimeout(() => {
279
+ this.localShippedItems = this.shippedItems.map(reduceItemBody)
280
+ const { amountSubtotal } = this
281
+ this.amountSubtotal = this.shippedItems.reduce((subtotal, item) => {
282
+ return subtotal + getPrice(item) * item.quantity
283
+ }, 0)
284
+ if (
285
+ this.hasCalculated &&
286
+ (this.canSelectServices || amountSubtotal !== this.amountSubtotal ||
287
+ (!this.shippingServices.length && !this.isWaiting))
288
+ ) {
289
+ this.fetchShippingServices()
290
+ }
291
+ }, 50)
292
+ },
293
+ deep: true,
294
+ immediate: true
295
+ },
296
+
177
297
  localZipCode (zipCode) {
178
298
  if (this.countryCode === 'BR' && zipCode.replace(/\D/g, '').length === 8) {
179
299
  this.submitZipCode()
@@ -189,6 +309,10 @@ export default {
189
309
  immediate: true
190
310
  },
191
311
 
312
+ skipAppIds () {
313
+ this.fetchShippingServices()
314
+ },
315
+
192
316
  shippingResult: {
193
317
  handler (result) {
194
318
  if (result.length) {