@lancom/shared 0.0.207 → 0.0.209

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 (25) hide show
  1. package/assets/js/api/admin.js +14 -2
  2. package/assets/js/utils/cart.js +2 -1
  3. package/assets/js/utils/gapis.js +61 -0
  4. package/components/checkout/cart/cart_entities_group/cart_entities_group_size_cell/cart-entities-group-size-cell.scss +1 -1
  5. package/components/checkout/cart/cart_entity/cart_entity_color_simple_products/cart_entity_color_simple_product/cart-entity-color-simple-product.scss +1 -1
  6. package/components/modals/payment_modal/payment-modal.vue +2 -0
  7. package/components/news/news_list_item/news-list-item.vue +6 -3
  8. package/components/product/gallery/gallery.scss +23 -4
  9. package/components/product/gallery/gallery.vue +13 -4
  10. package/components/product/product_reviews/add_review/add-review.scss +47 -0
  11. package/components/product/product_reviews/add_review/add-review.vue +227 -90
  12. package/components/product/product_reviews/product-reviews.vue +37 -3
  13. package/components/product/product_reviews/product_review/product-review.scss +8 -0
  14. package/components/product/product_reviews/product_review/product-review.vue +29 -2
  15. package/components/product/product_reviews/product_review/product_review_image/product-review-image.scss +0 -0
  16. package/components/product/product_reviews/product_review/product_review_image/product-review-image.vue +54 -0
  17. package/components/product/product_size_selector/product_size_selector_color/product_size_selector_color_cell/product-size-selector-color-cell.scss +4 -1
  18. package/components/product/product_size_selector/product_size_selector_color/product_size_selector_color_cell/product-size-selector-color-cell.vue +8 -0
  19. package/components/quotes/quote_view/quote-view.mixin.js +2 -0
  20. package/components/quotes/quote_view/quote-view.vue +5 -5
  21. package/mixins/payment.js +2 -0
  22. package/nuxt.config.js +125 -2
  23. package/package.json +1 -1
  24. package/plugins/directives.js +2 -0
  25. package/plugins/headers.js +4 -0
@@ -428,8 +428,17 @@ export default {
428
428
  fetchWarehouseLocations(params) {
429
429
  return _get('admin/warehouse-locations', params);
430
430
  },
431
- fetchWarehouseAllocations(warehouse, params) {
432
- return _get(`admin/warehouse/${warehouse}/allocations`, params);
431
+ fetchWarehouseAllocations(warehouse) {
432
+ return _get('admin/allocations', { warehouse });
433
+ },
434
+ createWarehouseAllocation(allocation) {
435
+ return _post(`admin/allocations`, allocation);
436
+ },
437
+ updateWarehouseAllocation(allocation) {
438
+ return _put(`admin/allocations/${allocation._id}`, allocation);
439
+ },
440
+ removeWarehouseAllocation(allocation) {
441
+ return _delete(`admin/allocations/${allocation._id}`, allocation);
433
442
  },
434
443
  fetchWarehouseLocationById(id) {
435
444
  return _get(`admin/warehouse-locations/${id}`);
@@ -443,6 +452,9 @@ export default {
443
452
  moveToWarehouseLocation(move) {
444
453
  return _post(`admin/warehouse-locations/move`, move);
445
454
  },
455
+ moveToWarehouseLocations(move) {
456
+ return _post(`admin/warehouse-locations/move/bulk`, move);
457
+ },
446
458
  async fetchBanners(params) {
447
459
  return sortByName(await _get('admin/banners', params));
448
460
  },
@@ -15,7 +15,8 @@ export function groupSimpleProducts(entity, isGroupByColor, isPopulateEmptyProdu
15
15
  simpleProducts: [],
16
16
  unitCosts: [],
17
17
  amount: 0,
18
- productsTotal: 0
18
+ productsTotal: 0,
19
+ name: simpleProduct.color?.name || simpleProduct.SKU
19
20
  };
20
21
  const group = groups.get(key) || defaultGroup;
21
22
 
@@ -0,0 +1,61 @@
1
+ function surveyOptin(order, shop) {
2
+ if (!window.renderOptIn) {
3
+ const tag = document.createElement('script');
4
+ tag.src = "https://apis.google.com/js/platform.js?onload=renderOptIn";
5
+ tag.defer = true;
6
+ tag.async = true;
7
+ const firstScriptTag = document.getElementsByTagName('script')[0];
8
+ firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
9
+
10
+ window.renderOptIn = () => {
11
+ surveyoptinRender(order, shop);
12
+ }
13
+ } else {
14
+ surveyoptinLoad(order, shop);
15
+ }
16
+ }
17
+
18
+ function surveyoptinLoad(order, shop) {
19
+ const { enabledSurveyoptin, MERCHANT_ID } = shop.settings.gapis || {};
20
+ if (enabledSurveyoptin && MERCHANT_ID) {
21
+ if (!window.gapi.surveyoptin.render) {
22
+ window.gapi.load('surveyoptin', function() {
23
+ surveyoptinRender(order, shop);
24
+ });
25
+ } else {
26
+ surveyoptinRender(order, shop);
27
+ }
28
+ }
29
+ }
30
+
31
+ function surveyoptinRender(order, shop) {
32
+ const { MERCHANT_ID } = shop.settings.gapis || {};
33
+ const products = order.products.reduce((gtins, { simpleProducts, product }) => {
34
+ return [
35
+ ...gtins,
36
+ ...(simpleProducts || [])
37
+ .filter(({ amount }) => amount > 0)
38
+ .map(({ SKU }) => {
39
+ const simpleProduct = (product?.simpleProducts || []).find(sp => sp.SKU === SKU);
40
+ return simpleProduct?.gtin;
41
+ })
42
+ .filter(gtin => !!gtin)
43
+ ];
44
+ }, []);
45
+ const estimated_delivery_date = "2023-11-28";
46
+ const data = {
47
+ merchant_id: MERCHANT_ID,
48
+ order_id: order.code,
49
+ email: order.shippingAddress.email,
50
+ delivery_country: order.shippingAddress.country,
51
+ estimated_delivery_date,
52
+ products,
53
+ opt_in_style: "BOTTOM_RIGHT_DIALOG"
54
+ };
55
+ console.log('surveyoptinRender: ', data);
56
+ window.gapi.surveyoptin.render(data);
57
+ }
58
+
59
+ export default {
60
+ surveyOptin
61
+ };
@@ -13,7 +13,7 @@
13
13
  height: 36px;
14
14
  padding: 2px !important;
15
15
  &.empty {
16
- color: #B0B0BA;
16
+ color: #8b8b8b;
17
17
  }
18
18
  }
19
19
  &__label {
@@ -45,7 +45,7 @@
45
45
  -moz-appearance: textfield;
46
46
  }
47
47
  &.empty {
48
- color: #B0B0BA;
48
+ color: #8b8b8b;
49
49
  }
50
50
  }
51
51
  &__price {
@@ -117,6 +117,7 @@
117
117
  import { mapGetters, mapActions, mapMutations } from 'vuex';
118
118
  import { price } from '@lancom/shared/assets/js/utils/filters';
119
119
  import gtm from '@lancom/shared/assets/js/utils/gtm';
120
+ import gapis from '@lancom/shared/assets/js/utils/gapis';
120
121
  import PaymentSuccess from '@lancom/shared/components/checkout/payment/payment_success/payment-success';
121
122
  import PaymentFailed from '@lancom/shared/components/checkout/payment/payment_failed/payment-failed';
122
123
 
@@ -253,6 +254,7 @@ export default {
253
254
  },
254
255
  sendConversionData() {
255
256
  gtm.purchase(this.orderData);
257
+ gapis.surveyOptin(this.orderData, this.shop);
256
258
  },
257
259
  back() {
258
260
  /*
@@ -4,14 +4,16 @@
4
4
  :style="{ backgroundImage }">
5
5
  <nuxt-link
6
6
  class="NewsListItem__overlay"
7
- :to="{ path: linkToNews }"></nuxt-link>
7
+ :to="{ path: linkToNews }"
8
+ :title="item.title"></nuxt-link>
8
9
  <div class="NewsListItem__info">
9
10
  <template v-if="!item.isHiddenThumbLink">
10
11
  <h1>
11
12
  <nuxt-link
12
13
  :to="{ path: linkToNews }"
13
14
  class="NewsListItem__link"
14
- :style="{ color: item.color }">
15
+ :style="{ color: item.color }"
16
+ :title="item.title">
15
17
  {{ item.title }}
16
18
  </nuxt-link>
17
19
  </h1>
@@ -29,7 +31,8 @@
29
31
  </div>
30
32
  <nuxt-link
31
33
  :to="{ path: linkToNews }"
32
- class="NewsListItem__more">
34
+ class="NewsListItem__more"
35
+ :title="item.title">
33
36
  <span>Read More</span>
34
37
  </nuxt-link>
35
38
  </article>
@@ -54,6 +54,7 @@
54
54
  &__small {
55
55
  overflow: hidden;
56
56
  position: relative;
57
+ margin: 20px 0;
57
58
  }
58
59
  &__zoom {
59
60
  position: absolute;
@@ -68,6 +69,7 @@
68
69
  justify-content: center;
69
70
  cursor: pointer;
70
71
  display: flex;
72
+ display: none !important;
71
73
  border: 1px solid grey;
72
74
  }
73
75
  &__big-image,
@@ -79,10 +81,27 @@
79
81
  background-size: contain;
80
82
  touch-action: auto !important;
81
83
  -ms-touch-action: auto !important;
82
- &::after {
83
- content: ' ';
84
- display: block;
85
- padding-top: 100%;
84
+ // &::after {
85
+ // content: ' ';
86
+ // display: block;
87
+ // padding-top: 100%;
88
+ // }
89
+ }
90
+ &__small-image {
91
+ height: 75px;
92
+ padding: 5px 0;
93
+ &-src {
94
+ max-width: 65px;
95
+ max-height: 65px;
96
+ margin: auto;
97
+ }
98
+ }
99
+ &__big-image {
100
+ height: 400px;
101
+ &-src {
102
+ max-width: 400px;
103
+ max-height: 400px;
104
+ margin: auto;
86
105
  }
87
106
  }
88
107
  &__big {
@@ -19,16 +19,21 @@
19
19
  v-hammer:pressup="onPanend"
20
20
  v-hammer:panend="onPanend"
21
21
  class="Gallery__big-image"
22
+ style="width: 300px; height: 300px;"
22
23
  :style="{
23
- 'background-image': `url(${staticLink(image.large)}`,
24
- 'background-size': backgroundSize,
25
- 'background-position': backgroundPosition,
24
+ // 'background-image': `url(${staticLink(image.large)}`,
25
+ // 'background-size': backgroundSize,
26
+ // 'background-position': backgroundPosition,
26
27
  'touch-action': zoomEnable ? 'none' : 'auto'
27
28
  }"
28
29
  @mousedown="onMouseDown"
29
30
  @mouseenter="onMouseEnter"
30
31
  @mouseleave="onMouseLeave"
31
32
  @mousemove="onMouseMove">
33
+ <img
34
+ :src="staticLink(image.large)"
35
+ class="Gallery__big-image-src"
36
+ />
32
37
  <div
33
38
  v-if="image.print"
34
39
  class="Gallery__big-print">
@@ -71,8 +76,12 @@
71
76
  }"
72
77
  @click="goToSlideAndChangeColor(index)"
73
78
  :style="{
74
- 'background-image': `url(${staticLink(image.small)}`
79
+ // 'background-image': `url(${staticLink(image.small)}`
75
80
  }">
81
+ <img
82
+ :src="staticLink(image.small)"
83
+ class="Gallery__small-image-src"
84
+ />
76
85
  <div class="Gallery__small-printed">
77
86
  {{ image.printType ? 'Printed' : (product.prePrint ? product.prePrintText : image.colorName || 'Blank') }}
78
87
  </div>
@@ -30,6 +30,53 @@
30
30
  margin-right: 22px;
31
31
  }
32
32
  }
33
+ &__upload {
34
+ display: flex;
35
+ &--uploading {
36
+ pointer-events: none;
37
+ opacity: 0.7;
38
+ }
39
+ &-btn {
40
+ font-weight: bold;
41
+ font-size: 16px;
42
+ line-height: 22px;
43
+ text-transform: uppercase;
44
+ background-color: $green;
45
+ padding: 8px 14px;
46
+ &--disabled {
47
+ background-color: $gray;
48
+ }
49
+ &--uploaded {
50
+ opacity: 0.5;
51
+ }
52
+ }
53
+ &-info {
54
+ font-size: 12px;
55
+ line-height: 16px;
56
+ color: $black;
57
+ margin-left: 12px;
58
+ }
59
+ &-progress {
60
+ position: absolute;
61
+ left: 0;
62
+ top: 0;
63
+ right: 0;
64
+ bottom: 0;
65
+ }
66
+ &-file {
67
+ margin-top: 5px;
68
+ font-size: 12px;
69
+ a {
70
+ text-decoration: none;
71
+ color: $black;
72
+ }
73
+ }
74
+ }
75
+ &__error {
76
+ margin-top: 5px;
77
+ font-size: 12px;
78
+ color: $error;
79
+ }
33
80
  }
34
81
  ::v-deep .AddReview__content {
35
82
  input,
@@ -9,100 +9,217 @@
9
9
  tag="form"
10
10
  class="AddReview__form">
11
11
  <div class="row">
12
- <div class="col-sm-6 col-12">
13
- <validation-provider
14
- v-slot="{ errors }"
15
- tag="div"
16
- name="Name"
17
- rules="required"
18
- class="form-row">
19
- <input
20
- id="add-review-name"
21
- ref="name"
22
- v-model="form.name"
23
- name="name"
24
- type="text"
25
- class="form-field"
26
- :class="{
27
- 'is-danger': errors.length,
28
- filled: form.name
29
- }"
30
- @keyup.enter="$refs.email.focus()" />
31
- <label
32
- for="add-review-name"
33
- class="form-label label-inner">
34
- Name
35
- </label>
36
- <span
37
- v-if="errors.length"
38
- class="form-help is-danger">
39
- {{ errors[0] }}
40
- </span>
41
- </validation-provider>
12
+ <div class="col-7">
13
+ <div class="row">
14
+ <div class="col-12">
15
+ <validation-provider
16
+ v-slot="{ errors }"
17
+ tag="div"
18
+ name="Name"
19
+ rules="required"
20
+ class="form-row">
21
+ <input
22
+ id="add-review-name"
23
+ ref="name"
24
+ v-model="form.name"
25
+ name="name"
26
+ type="text"
27
+ class="form-field"
28
+ :class="{
29
+ 'is-danger': errors.length,
30
+ filled: form.name
31
+ }"
32
+ @keyup.enter="$refs.email.focus()" />
33
+ <label
34
+ for="add-review-name"
35
+ class="form-label label-inner">
36
+ Name
37
+ </label>
38
+ <span
39
+ v-if="errors.length"
40
+ class="form-help is-danger">
41
+ {{ errors[0] }}
42
+ </span>
43
+ </validation-provider>
44
+ </div>
45
+ <div
46
+ class="col-12"
47
+ style="margin-top: -20px">
48
+ <validation-provider
49
+ v-slot="{ errors }"
50
+ tag="div"
51
+ name="Email"
52
+ rules="required|email"
53
+ class="form-row">
54
+ <input
55
+ id="add-review-email"
56
+ ref="email"
57
+ v-model="form.email"
58
+ name="email"
59
+ type="text"
60
+ class="form-field"
61
+ :class="{
62
+ 'is-danger': errors.length,
63
+ filled: form.email
64
+ }"
65
+ @keyup.enter="$refs.text.focus()" />
66
+ <label
67
+ for="add-review-email"
68
+ class="form-label label-inner">
69
+ Email (Not published)
70
+ </label>
71
+ <span
72
+ v-if="errors.length"
73
+ class="form-help is-danger">
74
+ {{ errors[0] }}
75
+ </span>
76
+ </validation-provider>
77
+ </div>
78
+ <div
79
+ class="col-12"
80
+ style="margin-top: -20px">
81
+ <validation-provider
82
+ v-slot="{ errors }"
83
+ tag="div"
84
+ name="Review"
85
+ rules="required|max:1024"
86
+ class="form-row">
87
+ <textarea
88
+ id="add-review-body"
89
+ ref="body"
90
+ v-model="form.text"
91
+ name="body"
92
+ class="form-textarea--size3"
93
+ :class="{
94
+ 'is-danger': errors.length,
95
+ filled: form.text
96
+ }">
97
+ </textarea>
98
+ <label
99
+ for="add-review-body"
100
+ class="form-label label-inner">
101
+ Your review...
102
+ </label>
103
+ <span
104
+ v-if="errors.length"
105
+ class="form-help is-danger">
106
+ {{ errors[0] }}
107
+ </span>
108
+ </validation-provider>
109
+ </div>
110
+ </div>
42
111
  </div>
43
- <div class="col-sm-6 col-12">
44
- <validation-provider
45
- v-slot="{ errors }"
46
- tag="div"
47
- name="Email"
48
- rules="required|email"
49
- class="form-row">
50
- <input
51
- id="add-review-email"
52
- ref="email"
53
- v-model="form.email"
54
- name="email"
55
- type="text"
56
- class="form-field"
57
- :class="{
58
- 'is-danger': errors.length,
59
- filled: form.email
60
- }"
61
- @keyup.enter="$refs.text.focus()" />
62
- <label
63
- for="add-review-email"
64
- class="form-label label-inner">
65
- Email (Not published)
66
- </label>
67
- <span
68
- v-if="errors.length"
69
- class="form-help is-danger">
70
- {{ errors[0] }}
71
- </span>
72
- </validation-provider>
112
+ <div class="col-5">
113
+ <div class="row">
114
+ <div class="col-12">
115
+ <div>
116
+ <validation-provider
117
+ v-slot="{ errors }"
118
+ tag="div"
119
+ name="Pro"
120
+ class="form-row">
121
+ <textarea
122
+ id="add-review-pro"
123
+ ref="pro"
124
+ v-model="form.pro"
125
+ name="pro"
126
+ class="form-field"
127
+ style="height: 125px;"
128
+ :class="{
129
+ 'is-danger': errors.length,
130
+ filled: form.pro
131
+ }"></textarea>
132
+ <label
133
+ for="add-review-pro"
134
+ class="form-label label-inner">
135
+ Pro
136
+ </label>
137
+ <span
138
+ v-if="errors.length"
139
+ class="form-help is-danger">
140
+ {{ errors[0] }}
141
+ </span>
142
+ </validation-provider>
143
+ </div>
144
+ <div class="mt-10">
145
+ <validation-provider
146
+ v-slot="{ errors }"
147
+ tag="div"
148
+ name="Cons"
149
+ class="form-row">
150
+ <textarea
151
+ id="add-review-cons"
152
+ ref="cons"
153
+ v-model="form.cons"
154
+ name="cons"
155
+ class="form-field"
156
+ style="height: 125px;"
157
+ :class="{
158
+ 'is-danger': errors.length,
159
+ filled: form.cons
160
+ }"></textarea>
161
+ <label
162
+ for="add-review-cons"
163
+ class="form-label label-inner">
164
+ Cons
165
+ </label>
166
+ <span
167
+ v-if="errors.length"
168
+ class="form-help is-danger">
169
+ {{ errors[0] }}
170
+ </span>
171
+ </validation-provider>
172
+ </div>
173
+ </div>
174
+ </div>
73
175
  </div>
74
176
  </div>
75
- <div class="row">
76
- <div class="col-12">
77
- <validation-provider
78
- v-slot="{ errors }"
79
- tag="div"
80
- name="Review"
81
- rules="required|max:1024"
82
- class="form-row">
83
- <textarea
84
- id="add-review-body"
85
- ref="body"
86
- v-model="form.text"
87
- name="body"
88
- class="form-textarea--size3"
177
+ <div class="form-row">
178
+ <file-uploader
179
+ url="reviews/image"
180
+ :multiple="false"
181
+ :has-conversion-error-modal="false"
182
+ :show-error-message="false"
183
+ @onchange="handleUploadChange"
184
+ @onerror="handleUploadError"
185
+ @onuploaded="handleUploaded">
186
+ <template v-slot:toggle="{ uploading }">
187
+ <div
188
+ class="AddReview__upload"
89
189
  :class="{
90
- 'is-danger': errors.length,
91
- filled: form.text
190
+ 'AddReview__upload--uploading': uploading
92
191
  }">
93
- </textarea>
94
- <label
95
- for="add-review-body"
96
- class="form-label label-inner">
97
- Your review...
98
- </label>
99
- <span
100
- v-if="errors.length"
101
- class="form-help is-danger">
102
- {{ errors[0] }}
103
- </span>
104
- </validation-provider>
192
+ <div
193
+ class="AddReview__upload-btn"
194
+ :class="{
195
+ 'AddReview__upload-btn--disabled': uploading,
196
+ 'AddReview__upload-btn--uploaded': form.image
197
+ }">
198
+ Upload
199
+ </div>
200
+ <div class="AddReview__upload-info">
201
+ <div>Note: accepted file types:</div>
202
+ <div><b>JPEG / PNG / AI / EPS / PDF</b></div>
203
+ </div>
204
+ </div>
205
+ </template>
206
+ <template v-slot:progress="{ progress }">
207
+ <div
208
+ v-if="progress"
209
+ class="AddReview__upload-progress">
210
+ <spinner background="black" />
211
+ </div>
212
+ </template>
213
+ </file-uploader>
214
+ <div
215
+ v-if="uploadError"
216
+ class="AddReview__error">
217
+ {{ uploadError }}
105
218
  </div>
219
+ <product-review-image
220
+ v-if="form.image"
221
+ style="margin-top: 10px;"
222
+ :review="form" />
106
223
  </div>
107
224
  <div class="AddReview__mark">
108
225
  <div class="AddReview__mark-label">
@@ -128,14 +245,18 @@
128
245
  </template>
129
246
 
130
247
  <script>
248
+ import FileUploader from '@lancom/shared/components/common/file_uploader';
131
249
  import StarsMark from '@lancom/shared/components/common/stars-mark';
132
250
  import api from '@lancom/shared/assets/js/api';
133
251
  import { mapGetters } from 'vuex';
252
+ import ProductReviewImage from './../product_review/product_review_image/product-review-image';
134
253
 
135
254
  export default {
136
255
  name: 'AddReview',
137
256
  components: {
138
- StarsMark
257
+ FileUploader,
258
+ StarsMark,
259
+ ProductReviewImage
139
260
  },
140
261
  props: {
141
262
  product: {
@@ -145,11 +266,16 @@ export default {
145
266
  },
146
267
  data() {
147
268
  return {
269
+ uploadError: null,
270
+ processing: false,
148
271
  form: {
149
272
  name: '',
150
273
  email: '',
151
274
  text: '',
152
- mark: 4
275
+ pro: '',
276
+ cons: '',
277
+ mark: 4,
278
+ image: null
153
279
  }
154
280
  };
155
281
  },
@@ -157,6 +283,17 @@ export default {
157
283
  ...mapGetters(['shop'])
158
284
  },
159
285
  methods: {
286
+ handleUploaded(file) {
287
+ this.form.image = file;
288
+ },
289
+ handleUploadError(e) {
290
+ const { error, message } = e?.response?.data || {};
291
+ this.uploadError = error || message || 'Failed upload image';
292
+ },
293
+ handleUploadChange() {
294
+ this.uploadError = null;
295
+ this.form.image = null;
296
+ },
160
297
  async submit() {
161
298
  try {
162
299
  this.processing = true;
@@ -39,6 +39,20 @@ export default {
39
39
  ProductReview
40
40
  },
41
41
  mixins: [],
42
+ data() {
43
+ const reviews = this.product.reviews || [];
44
+ const [, mainReviewId] = (this.$route.hash || '').match(/review-([a-z0-9]+)/i) || [];
45
+ const mainReview = reviews.find(review => review._id === mainReviewId);
46
+ console.log('mainReviewId: ', mainReviewId);
47
+ return {
48
+ scrollInterval: null,
49
+ mainReview,
50
+ reviews: [
51
+ mainReview,
52
+ ...reviews.filter(review => review !== mainReview)
53
+ ].filter(review => !!review)
54
+ };
55
+ },
42
56
  props: {
43
57
  product: {
44
58
  type: Object,
@@ -48,11 +62,31 @@ export default {
48
62
  computed: {
49
63
  hasReviews() {
50
64
  return this.reviews.length > 0;
51
- },
52
- reviews() {
53
- return this.product.reviews || [];
54
65
  }
55
66
  },
67
+ mounted() {
68
+ setTimeout(() => {
69
+ if (this.mainReview) {
70
+ const [first, second] = document.querySelectorAll(`#review-${this.mainReview._id}`) || [];
71
+ const mainReviewEl = second || first;
72
+ let top = 0;
73
+ if (mainReviewEl) {
74
+ this.scrollInterval = setInterval(() => {
75
+ top += 50;
76
+ window.scroll(0, top);
77
+ const rect = mainReviewEl.getBoundingClientRect();
78
+ console.log('mainReviewEl: ', mainReviewEl, rect.top);
79
+ if (rect.top < 300) {
80
+ clearInterval(this.scrollInterval);
81
+ }
82
+ }, 50);
83
+ }
84
+ }
85
+ }, 100);
86
+ },
87
+ destroyed() {
88
+ clearInterval(this.scrollInterval);
89
+ },
56
90
  methods: {
57
91
  showAddReviewModal() {
58
92
  const params = {
@@ -37,6 +37,14 @@
37
37
  margin-bottom: 10px;
38
38
  }
39
39
  }
40
+ &__info {
41
+ font-size: 11px;
42
+ margin-top: 6px;
43
+ color: grey;
44
+ }
45
+ &__info-wrapper {
46
+ display: flex;
47
+ }
40
48
  &__readmore {
41
49
  font-weight: 800;
42
50
  font-size: 16px;
@@ -1,11 +1,36 @@
1
1
  <template>
2
- <div class="ProductReview__wrapper">
2
+ <div
3
+ :id="`review-${review._id}`"
4
+ class="ProductReview__wrapper">
3
5
  <div class="ProductReview__name">
4
6
  {{ review.name }}
5
7
  </div>
6
8
  <div class="ProductReview__mark">
7
9
  <stars-mark v-model="review.mark" :disabled="true" />
8
10
  </div>
11
+ <div
12
+ v-if="review.pro || review.cons || review.image"
13
+ class="ProductReview__info-wrapper">
14
+ <div
15
+ v-if="review.image"
16
+ style="margin-right: 10px; margin-top: 7px;">
17
+ <product-review-image
18
+ :review="review"
19
+ :size="50" />
20
+ </div>
21
+ <div>
22
+ <div
23
+ v-if="review.pro"
24
+ class="ProductReview__info">
25
+ Pro: {{ review.pro }}
26
+ </div>
27
+ <div
28
+ v-if="review.cons"
29
+ class="ProductReview__info">
30
+ Cons: {{ review.cons }}
31
+ </div>
32
+ </div>
33
+ </div>
9
34
  <div
10
35
  ref="text"
11
36
  class="ProductReview__text"
@@ -24,11 +49,13 @@
24
49
  <script>
25
50
  import StarsMark from '@lancom/shared/components/common/stars-mark';
26
51
  import { nl2p } from '@lancom/shared/assets/js/utils/filters';
52
+ import ProductReviewImage from './product_review_image/product-review-image';
27
53
 
28
54
  export default {
29
55
  name: 'ProductReviews',
30
56
  components: {
31
- StarsMark
57
+ StarsMark,
58
+ ProductReviewImage
32
59
  },
33
60
  props: {
34
61
  review: {
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <div class="ProductReviewImage__wrapper">
3
+ <img
4
+ :src="review.image.small"
5
+ :style="{
6
+ width: `${size}px`
7
+ }"
8
+ @click="showImage">
9
+ </div>
10
+ </template>
11
+
12
+ <script>
13
+ import ImageViewer from '@lancom/shared/components/common/image_viewer/image-viewer';
14
+
15
+ export default {
16
+ name: 'ProductReviewImage',
17
+ props: {
18
+ review: {
19
+ type: Object,
20
+ required: true
21
+ },
22
+ size: {
23
+ type: Number,
24
+ default: 100
25
+ }
26
+ },
27
+ methods: {
28
+ showImage() {
29
+ this.$modal.show(
30
+ ImageViewer,
31
+ {
32
+ items: [{
33
+ src: this.review.image.large
34
+ }],
35
+ index: 0
36
+ },
37
+ {
38
+ name: 'image-viewer-modal',
39
+ root: this.$root,
40
+ width: '100%',
41
+ height: 'auto',
42
+ adaptive: true,
43
+ clickToClose: true,
44
+ transition: 'from-top-to-bottom'
45
+ }
46
+ );
47
+ }
48
+ }
49
+ };
50
+ </script>
51
+
52
+ <style lang="scss" scoped>
53
+ @import 'product-review-image.scss';
54
+ </style>
@@ -18,6 +18,9 @@
18
18
  }
19
19
  }
20
20
  }
21
+ &__lable {
22
+ display: none;
23
+ }
21
24
  &__field {
22
25
  position: relative;
23
26
  width: 100%;
@@ -31,7 +34,7 @@
31
34
  opacity: .3;
32
35
  }
33
36
  &.empty {
34
- color: $grey_2;
37
+ color: #8b8b8b;
35
38
  }
36
39
  }
37
40
  }
@@ -12,13 +12,20 @@
12
12
  @click="model -= step">
13
13
  <span> - </span>
14
14
  </div>
15
+ <label
16
+ :for="uniqueFieldId"
17
+ class="ProductSizeSelectorColorCell__lable">
18
+ {{ size.name }}
19
+ </label>
15
20
  <input
21
+ :id="uniqueFieldId"
16
22
  v-model.number="model"
17
23
  type="number"
18
24
  min="0"
19
25
  max="9999"
20
26
  class="form-field"
21
27
  :class="{ invalidate: disabled, empty: !model }"
28
+ :aria-labelledby="size.name"
22
29
  :disabled="disabled"
23
30
  @focus="onFocus()"
24
31
  @blur="onBlur()" />
@@ -76,6 +83,7 @@ export default {
76
83
  },
77
84
  data() {
78
85
  return {
86
+ uniqueFieldId: `size-selector-${this.color._id}-${this.size._id}`,
79
87
  defaultValue: 0
80
88
  };
81
89
  },
@@ -1,6 +1,7 @@
1
1
  import { mapGetters, mapActions, mapMutations } from 'vuex';
2
2
  import api from '@lancom/shared/assets/js/api';
3
3
  import gtm from '@lancom/shared/assets/js/utils/gtm';
4
+ import gapis from '@lancom/shared/assets/js/utils/gapis';
4
5
  import { price, shortDate, tax } from '@lancom/shared/assets/js/utils/filters';
5
6
  import { convertQuoteToOrder } from '@lancom/shared/assets/js/utils/quote';
6
7
 
@@ -37,6 +38,7 @@ export default {
37
38
  this.order = await this.createOrder(option);
38
39
  this.setOrder(this.order);
39
40
  gtm.purchase(this.order);
41
+ gapis.surveyOptin(this.order, this.shop);
40
42
  this.clear();
41
43
  } catch (e) {
42
44
  const { message } = (e.response && e.response.data) || e;
@@ -30,12 +30,12 @@
30
30
  <tr>
31
31
  <td>
32
32
  <div><b>QUOTE</b></div>
33
- <div>{{ quote.fullName }}</div>
34
- <div v-if="quote.company">{{ quote.company }}</div>
35
- <div v-if="quote.phone">{{ quote.phone }}</div>
36
- <div>{{ quote.email }}</div>
33
+ <div>{{ quote.address.fullName }}</div>
34
+ <div v-if="quote.address.company">{{ quote.address.company }}</div>
35
+ <div v-if="quote.address.phone">{{ quote.address.phone }}</div>
36
+ <div>{{ quote.address.email }}</div>
37
37
  <div>{{ quoteAddress }}</div>
38
- <div>{{ quote.additionalInfo }}</div>
38
+ <div>{{ quote.address.additionalInfo }}</div>
39
39
  </td>
40
40
  <td class="w-50">
41
41
  <div><b>DIRECT DEPOSIT DETAILS</b></div>
package/mixins/payment.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mapGetters, mapActions, mapMutations } from 'vuex';
2
2
  import gtm from '@lancom/shared/assets/js/utils/gtm';
3
+ import gapis from '@lancom/shared/assets/js/utils/gapis';
3
4
  import { ORDER_PAYMENT_METHOD } from '@lancom/shared/assets/js/constants/order';
4
5
 
5
6
  export default {
@@ -64,6 +65,7 @@ export default {
64
65
  },
65
66
  sendConversionData() {
66
67
  gtm.purchase(this.orderData);
68
+ gapis.surveyOptin(this.orderData, this.shop);
67
69
  },
68
70
  clearFailedCharge() {
69
71
  this.errorMessage = null;
package/nuxt.config.js CHANGED
@@ -65,6 +65,9 @@ module.exports = (config, axios, { raygunClient, publicPath } = {}) => ({
65
65
  { src: '@/node_modules/@lancom/shared/plugins/vue-recaptcha', ssr: false },
66
66
  // { src: '@/node_modules/@lancom/shared/plugins/vue-tables-2', ssr: false }
67
67
  ],
68
+ serverMiddleware: [
69
+ '@/node_modules/@lancom/shared/plugins/headers'
70
+ ],
68
71
  modules: [
69
72
  'nuxt-helmet',
70
73
  '@nuxtjs/axios',
@@ -99,7 +102,7 @@ module.exports = (config, axios, { raygunClient, publicPath } = {}) => ({
99
102
  config.devtool = 'source-map';
100
103
  }
101
104
  },
102
- extractCSS: true,
105
+ extractCSS: !config.IS_LOCAL,
103
106
  },
104
107
  hooks: {
105
108
  render: {
@@ -111,12 +114,120 @@ module.exports = (config, axios, { raygunClient, publicPath } = {}) => ({
111
114
  },
112
115
  },
113
116
  feed: [{
117
+ path: '/pr-rev-au.xml',
118
+ async get() {
119
+ const { data } = await axios.get(`${config.LOCAL_API_URL}/feed/reviews?host=${config.HOST_NAME}`);
120
+
121
+ return {
122
+ _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } },
123
+ feed: {
124
+ _attributes: {
125
+ 'xmlns:vc': 'http://www.w3.org/2007/XMLSchema-versioning',
126
+ 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
127
+ 'xsi:noNamespaceSchemaLocation': 'http://www.google.com/shopping/reviews/schema/product/2.3/product_reviews.xsd'
128
+ },
129
+ version: { _text: '2.3' },
130
+ publisher: {
131
+ name: { _text: 'Workdepot Australia' },
132
+ favicon: { _text: 'https://www.workdepot.com.au/favicon.png' }
133
+ },
134
+ reviews: {
135
+ review: [
136
+ ...data
137
+ .filter(review => !!review.product)
138
+ .map(review => {
139
+ const { product } = review;
140
+ const productUrl = `https://${config.HOST_NAME}/${product.brand.alias}/${product.productType.alias}/${product.alias}`;
141
+ const item = {
142
+ review_id: { _text: review._id },
143
+ reviewer: {
144
+ name: {
145
+ _attributes: {
146
+ is_anonymous: 'false'
147
+ },
148
+ _text: review.name
149
+ }
150
+ },
151
+ review_timestamp: { _text: review.createdAt },
152
+ title: { _text: product.name },
153
+ content: { _text: review.text },
154
+ review_url: {
155
+ _attributes: {
156
+ type: 'singleton'
157
+ },
158
+ _text: `${productUrl}#review-${review._id}`
159
+ },
160
+ ratings: {
161
+ overall: {
162
+ _attributes: {
163
+ min: 1,
164
+ max: 5
165
+ },
166
+ _text: review.mark
167
+ }
168
+ },
169
+ products: {
170
+ product: {
171
+ product_ids: {
172
+ brands: {
173
+ brand: { _text: product.brand.name }
174
+ },
175
+ // mpns: {
176
+ // mpn: { _text: '60101-10000' }
177
+ // },
178
+ // asins: {
179
+ // asin: { _text: 'B07YMJ57MB' }
180
+ // }
181
+ },
182
+ product_name: { _text: product.name },
183
+ product_url: { _text: productUrl }
184
+ }
185
+ }
186
+ };
187
+
188
+ if (product.simpleProduct?.gtin) {
189
+ item.products.product.product_ids.gtins = {
190
+ gtin: { _text: product.simpleProduct?.gtin }
191
+ };
192
+ }
193
+
194
+ if (product.simpleProduct?.SKU) {
195
+ item.products.product.product_ids.skus = {
196
+ sku: { _text: product.simpleProduct?.SKU }
197
+ };
198
+ }
199
+
200
+ if (review.pro) {
201
+ item.pros = {
202
+ pro: [{ _text: review.pro }]
203
+ };
204
+ }
205
+ if (review.cons) {
206
+ item.cons = {
207
+ con: [{ _text: review.cons }]
208
+ };
209
+ }
210
+ if (review.image) {
211
+ item.reviewer_images = {
212
+ reviewer_image: [{
213
+ url: { _text: review.image.large }
214
+ }]
215
+ };
216
+ }
217
+ return item;
218
+ })
219
+ ]
220
+ }
221
+ }
222
+ };
223
+ }
224
+ }, {
114
225
  path: '/google-shopping.xml',
115
226
  async get() {
116
227
  const { data } = await axios.get(`${config.LOCAL_API_URL}/feed/products?host=${config.HOST_NAME}`);
117
228
  const spliceFirstImage = images => (images || []).splice(0, 1)[0];
118
229
  const getImages = images => (images || []).length > 0 ? images : null;
119
- return {
230
+ const channel = {
120
231
  title: { _text: 'All products' },
121
232
  link: { _text: `https://${config.HOST_NAME}` },
122
233
  generator: { _text: config.HOST_NAME },
@@ -183,6 +294,18 @@ module.exports = (config, axios, { raygunClient, publicPath } = {}) => ({
183
294
  ];
184
295
  }, [])
185
296
  };
297
+
298
+ return {
299
+ _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } },
300
+ rss: {
301
+ _attributes: { version: "2.0", 'xmlns:g': "http://base.google.com/ns/1.0" },
302
+ channel: {
303
+ lastBuildDate: { _text: new Date().toUTCString() },
304
+ docs: { _text: "https://validator.w3.org/feed/docs/rss2.html" },
305
+ ...channel
306
+ },
307
+ },
308
+ }
186
309
  }
187
310
  }],
188
311
  router: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lancom/shared",
3
- "version": "0.0.207",
3
+ "version": "0.0.209",
4
4
  "description": "lancom common scripts",
5
5
  "author": "e.tokovenko <e.tokovenko@gmail.com>",
6
6
  "repository": {
@@ -1,5 +1,7 @@
1
1
  import Vue from 'vue';
2
2
 
3
+ Vue.config.productionTip = false;
4
+
3
5
  Vue.directive('focus', {
4
6
  inserted: (el, binding) => {
5
7
  const delay = binding.value || 500;
@@ -0,0 +1,4 @@
1
+ module.exports = function (req, res, next) {
2
+ res.removeHeader('Expect-CT');
3
+ next();
4
+ };