@lancom/shared 0.0.351 → 0.0.353

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 (28) hide show
  1. package/assets/js/utils/colors.js +1 -1
  2. package/assets/js/utils/fonts-helper.js +2 -1
  3. package/assets/js/utils/product.js +10 -2
  4. package/components/editor/editor_layers/editor_layer_forms/editor_layer_form_text/editor-layer-form-text.vue +1 -1
  5. package/components/editor/editor_product_details/editor-product-details.scss +13 -0
  6. package/components/editor/editor_product_details/editor-product-details.vue +28 -1
  7. package/components/editor/editor_workspace/editor-workspace.scss +5 -1
  8. package/components/editor/editor_workspace/editor-workspace.vue +25 -14
  9. package/components/editor/editor_workspace/editor_workspace_side/editor-workspace-side.scss +27 -4
  10. package/components/editor/editor_workspace/editor_workspace_side/editor-workspace-side.vue +68 -19
  11. package/components/product/gallery/gallery.scss +6 -0
  12. package/components/product/gallery/gallery.vue +29 -5
  13. package/components/product/layouts/product_main_info/product-main-info.vue +2 -2
  14. package/components/product/product.vue +17 -2
  15. package/components/product/product_color_image/product-color-image.vue +7 -2
  16. package/components/products/products_aside/products-aside.vue +15 -1
  17. package/components/products/products_link/products-link.vue +8 -0
  18. package/components/products/products_minimum_qty/products-minimum-qty.scss +39 -0
  19. package/components/products/products_minimum_qty/products-minimum-qty.vue +88 -0
  20. package/components/products/products_production_time/products-production-time.scss +39 -0
  21. package/components/products/products_production_time/products-production-time.vue +85 -0
  22. package/feeds/google-shopping.js +7 -1
  23. package/layouts/products.vue +5 -3
  24. package/middleware/page-info.js +16 -3
  25. package/mixins/product-view.js +10 -4
  26. package/package.json +1 -1
  27. package/plugins/cache-headers.js +6 -2
  28. package/store/product.js +10 -0
@@ -11,7 +11,7 @@ export const getColorImage = (product = {}, size = 'small', type, color, allowAn
11
11
  const validImages = (product.images || []).filter(i => !excludeTypes.some(type => isValidImageType(i, type)));
12
12
  const colorImage = color && validImages.find(i => isValidImageType(i, type || COLORS_IMAGES_TYPES.FRONT) && (color?._id === i.color || color?._id === i.color?._id));
13
13
  const image = colorImage || validImages.find(i => isValidImageType(i, type || COLORS_IMAGES_TYPES.FRONT) && (allowAnyColor || !i.color));
14
- return image && staticLink(image[size]);
14
+ return image && image[size] && staticLink(image[size]);
15
15
  };
16
16
 
17
17
  export const getProductCover = (product = {}, size = 'small', type = COLORS_IMAGES_TYPES.FRONT, color, allowAnyColor = false) => {
@@ -65,7 +65,8 @@ export const getFormattedFont = alias => {
65
65
  export const getFontPath = alias => {
66
66
  const { PROD_CDN_URL, DEV_CDN_URL, CDN_DOMAIN } = Vue.$env;
67
67
  const { path } = getFont(alias) || {};
68
- return path && staticLink(`${CDN_DOMAIN || PROD_CDN_URL || DEV_CDN_URL || ''}${path}`);
68
+ const domain = process.env.IS_LOCAL ? '' : (CDN_DOMAIN || PROD_CDN_URL || DEV_CDN_URL || '');
69
+ return path && staticLink(`${domain}${path}`);
69
70
  };
70
71
 
71
72
  export const loadFont = alias => new Promise((resolve, reject) => {
@@ -110,10 +110,12 @@ function getCategoryPathFromRoute(routeParams) {
110
110
  }
111
111
 
112
112
  export function generateProductsLink($route, data, skipParams = false) {
113
- let { type, category, brand, text, sort } = data;
113
+ let { type, category, brand, text, sort, minimumQty, productionTime } = data;
114
114
  category = (category || category === null) ? generateCategoryPath(category) : getCategoryPathFromRoute($route?.params);
115
115
  type = (type || type === null) ? type : ($route?.params?.type || $route?.query?.type);
116
116
  brand = (brand || brand === null) ? brand : ($route?.params?.brand || $route?.query?.brand);
117
+ minimumQty = (minimumQty || minimumQty === null) ? minimumQty : ($route?.params?.minimumQty || $route?.query?.minimumQty);
118
+ productionTime = (productionTime || productionTime === null) ? productionTime : ($route?.params?.productionTime || $route?.query?.productionTime);
117
119
  text = (text || text === '') ? text : $route?.query?.text;
118
120
  sort = (sort || sort === '') ? sort : $route?.query?.sort;
119
121
 
@@ -125,6 +127,12 @@ export function generateProductsLink($route, data, skipParams = false) {
125
127
  if (sort) {
126
128
  params.push(`sort=${sort}`);
127
129
  }
130
+ if (minimumQty) {
131
+ params.push(`minimumQty=${minimumQty}`);
132
+ }
133
+ if (productionTime) {
134
+ params.push(`productionTime=${productionTime}`);
135
+ }
128
136
 
129
137
  const { page = 1 } = data;
130
138
  if (page > 1) {
@@ -132,7 +140,7 @@ export function generateProductsLink($route, data, skipParams = false) {
132
140
  }
133
141
 
134
142
  Object.keys({ ...(($route && $route.query) || {}), ...data }).forEach(key => {
135
- const rootKeys = ['text', 'sort', 'page', 'category', 'type', 'brand'];
143
+ const rootKeys = ['text', 'sort', 'page', 'category', 'type', 'brand', 'minimumQty', 'productionTime'];
136
144
  if (!rootKeys.includes(key)) {
137
145
  let items = ($route && $route.query[key]) ? $route.query[key].split(',') : [];
138
146
  items = (data[key] ? (items.includes(data[key]) ? items.filter(c => c !== data[key]) : [...items, data[key]]) : items).join(',');
@@ -5,7 +5,7 @@
5
5
  name="header"
6
6
  :invalid="invalid">
7
7
  </slot>
8
- <div class="EditorLayerFormText__content">
8
+ <div class="EditorLayerFormText__content" @click.stop>
9
9
  <validation-provider
10
10
  tag="div"
11
11
  name="Copy"
@@ -13,6 +13,19 @@
13
13
  margin-top: -10px;
14
14
  }
15
15
  }
16
+ &__colors {
17
+ position: relative;
18
+ }
19
+ &__loader {
20
+ position: absolute;
21
+ top: 0;
22
+ right: 0;
23
+ bottom: 0;
24
+ left: 0;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ }
16
29
  &__description {
17
30
  margin-top: 15px;
18
31
  color: rgb(118, 118, 118);
@@ -125,9 +125,19 @@
125
125
  <i class="icon-rotate-tee"></i>
126
126
  </div>
127
127
  </div>
128
+ <div class="EditorProductDetails__section">
129
+ <client-only>
130
+ <gallery
131
+ v-if="hasImages"
132
+ :show-big-image="false"
133
+ :image-types="['model']"
134
+ :show-filters="false" />
135
+ </client-only>
136
+ </div>
128
137
  <div
129
138
  v-if="productDetailsLoaded"
130
- id="EditorProductDetails">
139
+ id="EditorProductDetails"
140
+ class="EditorProductDetails__colors">
131
141
  <div class="EditorProductDetails__section">
132
142
  <product-colors-selector
133
143
  :has-another-print-btn="false"
@@ -136,6 +146,13 @@
136
146
  <div class="EditorProductDetails__section">
137
147
  <editor-pricing :has-cart-btn="false" />
138
148
  </div>
149
+ <div
150
+ v-if="loadingProductDetails"
151
+ class="EditorProductDetails__loader">
152
+ <div>
153
+ <spinner />
154
+ </div>
155
+ </div>
139
156
  </div>
140
157
  </div>
141
158
  </template>
@@ -153,10 +170,13 @@ import Price from '@lancom/shared/components/common/price';
153
170
  import ProductSideWithPrint from '@lancom/shared/components/common/product_side_with_print/product-side-with-print';
154
171
  import RichText from '@lancom/shared/components/common/rich-text';
155
172
  import QuoteRequestModal from '@lancom/shared/components/quotes/quote_request_modal/quote-request-modal';
173
+ import Gallery from '@lancom/shared/components/product/gallery/gallery';
174
+ import { isValidImageType } from '@lancom/shared/assets/js/utils/colors';
156
175
 
157
176
  export default {
158
177
  name: 'EditorProductDetails',
159
178
  components: {
179
+ Gallery,
160
180
  ProductColorsSelector,
161
181
  PricingDiscountsTable,
162
182
  EditorPricing,
@@ -179,6 +199,7 @@ export default {
179
199
  computed: {
180
200
  ...mapGetters(['taxName']),
181
201
  ...mapGetters('product', [
202
+ 'loadingProductDetails',
182
203
  'product',
183
204
  'editableColor',
184
205
  'images',
@@ -197,6 +218,12 @@ export default {
197
218
  'pricingSettings',
198
219
  'country'
199
220
  ]),
221
+ hasImages() {
222
+ return this.modelImages.length > 0;
223
+ },
224
+ modelImages() {
225
+ return this.images.filter(i => isValidImageType(i, 'model'));
226
+ },
200
227
  previewPrintProduct() {
201
228
  return {
202
229
  ...this.product,
@@ -85,7 +85,11 @@
85
85
  position: fixed;
86
86
  right: 20px;
87
87
  top: 20px;
88
- font-size: 28px;
88
+ font-size: 18px;
89
+ display: flex;
90
+ .Btn__wrapper {
91
+ height: 40px;
92
+ }
89
93
  }
90
94
  &__side {
91
95
  position: absolute;
@@ -84,9 +84,14 @@
84
84
  </div>
85
85
  <div
86
86
  v-if="isZoomIn"
87
- class="EditorWorkspace__zoom-out"
88
- @click="zoomOut()">
89
- <i class="icon-cancel"></i>
87
+ class="EditorWorkspace__zoom-out">
88
+ <btn
89
+ btn-class="green"
90
+ btn-label="Save"
91
+ @onclick="zoomOut()" />
92
+ <btn
93
+ btn-label="Cancel"
94
+ @onclick="zoomOut(true)" />
90
95
  </div>
91
96
  </div>
92
97
  </client-only>
@@ -136,14 +141,7 @@ export default {
136
141
  size: null,
137
142
  fabricHelper: null,
138
143
  preloading: true,
139
- isRotating: false,
140
- productSides: [
141
- { label: 'Front', value: 'front' },
142
- { label: 'Back', value: 'back' }
143
- // { label: 'Left sleeve', value: 'left_sleeve' },
144
- // { label: 'Right sleeve', value: 'right_sleeve' },
145
- // { label: 'Outside label', value: 'outside_label' }
146
- ]
144
+ isRotating: false
147
145
  };
148
146
  },
149
147
  props: {
@@ -156,6 +154,7 @@ export default {
156
154
  ...mapGetters([
157
155
  'product',
158
156
  'selectedLayer',
157
+ 'selectedLayerCopy',
159
158
  'productDetailsLoaded',
160
159
  'selectedPrintArea',
161
160
  'editablePrintArea',
@@ -164,6 +163,14 @@ export default {
164
163
  'editableLayers',
165
164
  'editorSize'
166
165
  ]),
166
+ productSides() {
167
+ const sides = [
168
+ { label: 'Front', value: 'front' },
169
+ { label: 'Back', value: 'back' }
170
+ ]
171
+ const validSides = sides.filter(s => this.product.printAreas?.some(pa => pa.side === s.value));
172
+ return validSides[0] ? validSides : [sides[0]];
173
+ },
167
174
  sideZoomSize() {
168
175
  return this.printAreaZoomSize?.width;
169
176
  },
@@ -216,10 +223,14 @@ export default {
216
223
  'setEditableSide',
217
224
  'setSelectedPrintArea',
218
225
  'setEditablePrintArea',
219
- 'setSelectedLayer'
226
+ 'setSelectedLayer',
227
+ 'updateTemplateLayer'
220
228
  ]),
221
- zoomOut() {
222
- this.setSelectedLayer(null);
229
+ zoomOut(isCancel) {
230
+ if (isCancel) {
231
+ this.updateTemplateLayer(this.selectedLayerCopy);
232
+ }
233
+ setTimeout(() => this.setSelectedLayer(null), 100);
223
234
  },
224
235
  onWorkspaceChange({ size, fabricHelper }) {
225
236
  this.fabricHelper = fabricHelper;
@@ -6,14 +6,20 @@
6
6
  transition: background-color 0.5s ease;
7
7
  background-color: white;
8
8
  }
9
- &__toggle-wireframe {
9
+ &__toggle-bg {
10
10
  position: absolute;
11
+ top: 5px;
12
+ display: flex;
13
+ left: 0;
14
+ right: 0;
15
+ }
16
+ &__toggle-wireframe {
11
17
  z-index: 1001;
12
18
  background: gainsboro;
13
19
  padding: 6px;
14
- top: 5px;
15
20
  font-size: 14px;
16
21
  cursor: pointer;
22
+ margin-right: 5px;
17
23
  &--active {
18
24
  background: rgb(196, 226, 236);
19
25
  }
@@ -88,6 +94,9 @@
88
94
  display: inline;
89
95
  }
90
96
  }
97
+ &:last-child {
98
+ display: none;
99
+ }
91
100
  }
92
101
  &-divider {
93
102
  color: $grey_1;
@@ -110,10 +119,17 @@
110
119
  }
111
120
  }
112
121
  &.side {
113
- width: 20px;
122
+ display: flex;
123
+ flex-direction: row;
114
124
  .EditorWorkspaceSide__placeholder-option span {
115
125
  &:last-child {
116
- display: inline;
126
+ display: inline-block;
127
+ background-color: $purple;
128
+ color: $white;
129
+ cursor: pointer;
130
+ padding: 4px;
131
+ margin: 1px;
132
+ font-size: 15px;
117
133
  }
118
134
  &:first-child {
119
135
  display: none;
@@ -122,6 +138,13 @@
122
138
  .EditorWorkspaceSide__placeholder-divider {
123
139
  display: none;
124
140
  }
141
+ .EditorWorkspaceSide__placeholder-option--edit-btn {
142
+ display: flex;
143
+ span {
144
+ margin-top: -0.5px !important;
145
+ display: inline-block !important;
146
+ }
147
+ }
125
148
  }
126
149
  }
127
150
  &__alert {
@@ -7,15 +7,29 @@
7
7
  }"
8
8
  v-click-outside="onOutsideClick"
9
9
  @mouseover="toogleBoundBox(true)"
10
- @mouseleave="toogleBoundBox(false)">
10
+ @mouseleave="toogleBoundBox(false)"
11
+ @mouseup="toggleOnpressImage(false)">
11
12
  <div
12
- v-if="hasWireframeImage"
13
- class="EditorWorkspaceSide__toggle-wireframe"
14
- :class="{
15
- 'EditorWorkspaceSide__toggle-wireframe--active': visibleWireframe
16
- }"
17
- @click.stop.prevent="toggleWireframeImage">
18
- {{ visibleWireframe ? 'Hide Wireframe' : 'Show Wireframe' }}
13
+ v-if="hasOnpressImage || hasWireframeImage"
14
+ class="EditorWorkspaceSide__toggle-bg">
15
+ <div
16
+ v-if="hasWireframeImage"
17
+ class="EditorWorkspaceSide__toggle-wireframe"
18
+ :class="{
19
+ 'EditorWorkspaceSide__toggle-wireframe--active': visibleWireframe
20
+ }"
21
+ @click.stop.prevent="toggleWireframeImage">
22
+ {{ visibleWireframe ? 'Hide Wireframe' : 'Show Wireframe' }}
23
+ </div>
24
+ <div
25
+ v-if="hasOnpressImage"
26
+ class="EditorWorkspaceSide__toggle-wireframe"
27
+ :class="{
28
+ 'EditorWorkspaceSide__toggle-wireframe--active': visibleOnpress
29
+ }"
30
+ @mousedown.stop.prevent="toggleOnpressImage(true)">
31
+ {{ visibleOnpress ? 'Hide On Press' : 'Show On Press' }}
32
+ </div>
19
33
  </div>
20
34
  <div
21
35
  v-if="deleteButtonPos"
@@ -33,7 +47,7 @@
33
47
  </div>
34
48
  </div>
35
49
  <div
36
- v-if="fabricHelper"
50
+ v-if="fabricHelper && !editModeSelectedLayer"
37
51
  class="EditorWorkspaceSide__placeholder"
38
52
  :class="{
39
53
  tighten: !isZoomed && printAreaIsSmall,
@@ -45,7 +59,7 @@
45
59
  @click="createTextLayer"
46
60
  class="EditorWorkspaceSide__placeholder-option">
47
61
  <span>Type here</span>
48
- <span>T</span>
62
+ <span style="margin-top: -0.5px;">Text</span>
49
63
  </div>
50
64
  <div class="EditorWorkspaceSide__placeholder-divider">
51
65
  or
@@ -61,7 +75,7 @@
61
75
  <i class="icon-picture"></i> Add art
62
76
  </span>
63
77
  <span>
64
- <i class="icon-picture"></i>
78
+ Art
65
79
  </span>
66
80
  </div>
67
81
  </template>
@@ -77,6 +91,12 @@
77
91
  </template>
78
92
  </file-uploader>
79
93
  </div>
94
+ <!-- <div
95
+ v-if="editablePrintAreaLayers"
96
+ @click="editFirstLayer"
97
+ class="EditorWorkspaceSide__placeholder-option EditorWorkspaceSide__placeholder-option--edit-btn">
98
+ <span>Edit</span>
99
+ </div> -->
80
100
  </div>
81
101
  <div
82
102
  v-show="isVisibleOverlay"
@@ -143,6 +163,7 @@ export default {
143
163
  return {
144
164
  addedFromCanvas: false,
145
165
  visibleWireframe: false,
166
+ visibleOnpress: false,
146
167
  fabricHelper: null,
147
168
  redrawWithThrottle: throttle(this.redraw, 100, { trailing: false }),
148
169
  saveLayersAsImageWithDebounce: debounce(this.saveLayersAsImage, 400),
@@ -165,6 +186,7 @@ export default {
165
186
  'editableColor',
166
187
  'editableLayers',
167
188
  'selectedLayer',
189
+ 'selectedLayerCopy',
168
190
  'editorSize',
169
191
  'editModeSelectedLayer'
170
192
  ]),
@@ -174,12 +196,22 @@ export default {
174
196
  hasWireframeImage() {
175
197
  return !!this.wireframeImage;
176
198
  },
199
+ hasOnpressImage() {
200
+ return !!this.onpressImage;
201
+ },
177
202
  wireframeImage() {
178
203
  return this.images.find(i => i.types?.includes('wireframe') && i.types?.includes(this.side));
179
204
  },
205
+ onpressImage() {
206
+ console.log('this.images: ', this.images);
207
+ return this.product.images?.find(i => i.types?.includes('on_press'));
208
+ },
180
209
  sideEditableLayers() {
181
210
  return this.editableLayers.filter(l => l.sideId === this.side);
182
211
  },
212
+ editablePrintAreaLayers() {
213
+ return this.printAreaLayers.some(l => l.editableDetails);
214
+ },
183
215
  printAreaLayers() {
184
216
  const paId = this.printArea?.parentPrintArea || this.printArea?._id;
185
217
  const layers = this.editableLayers.filter(l => !paId || (l.printArea === paId));
@@ -196,8 +228,8 @@ export default {
196
228
  // console.log('this.fabricHelper.printAreaRect: ', this.fabricHelper.printAreaRect);
197
229
  const ratio = this.calcWorkspaceSize() / this.editorSize.width;
198
230
  return this.printAreaLayers.length > 0 ? {
199
- top: `${top * ratio}px`,
200
- left: `${(left + width) * ratio + 4}px`
231
+ top: `${(top + height) * ratio}px`,
232
+ left: `${left * ratio}px`
201
233
  } : {
202
234
  top: `${top * ratio}px`,
203
235
  left: `${left * ratio}px`,
@@ -241,6 +273,9 @@ export default {
241
273
  visibleWireframe() {
242
274
  this.updateBackgroundImage();
243
275
  },
276
+ visibleOnpress() {
277
+ this.updateBackgroundImage();
278
+ },
244
279
  layers() {
245
280
  this.redrawWithThrottle();
246
281
  },
@@ -275,18 +310,29 @@ export default {
275
310
  ...mapActions('product', ['createLayer']),
276
311
  ...mapMutations('product', [
277
312
  'setSelectedLayerField',
313
+ 'setEditModeSelectedLayer',
278
314
  'setSelectedLayer',
279
315
  'removeTemplateLayer'
280
316
  ]),
281
317
  ...mapMutations('layers', [
282
318
  'setLayersThumbnail'
283
319
  ]),
320
+ editFirstLayer() {
321
+ const layer = this.printAreaLayers.find(l => l.editableDetails);
322
+ if (layer?.editableDetails) {
323
+ this.setSelectedLayer(layer);
324
+ this.setEditModeSelectedLayer(true);
325
+ }
326
+ },
284
327
  checkVisibleWireframe() {
285
328
  this.visibleWireframe = this.sideEditableLayers.length > 0;
286
329
  },
287
330
  toggleWireframeImage() {
288
331
  this.visibleWireframe = !this.visibleWireframe;
289
332
  },
333
+ toggleOnpressImage(state) {
334
+ this.visibleOnpress = state;
335
+ },
290
336
  adjustSelectedArtDPI() {
291
337
  const measure = this.fabricHelper.getEditorDPI();
292
338
  const scale = measure / 75;
@@ -385,19 +431,22 @@ export default {
385
431
  const isMedium = this.calcWorkspaceSize(true) <= 400;
386
432
  const type = this.side === 'front' ? COLORS_IMAGES_TYPES.FRONT : COLORS_IMAGES_TYPES.BACK;
387
433
  const wireframeImageUrl = this.visibleWireframe && (isMedium ? this.wireframeImage?.medium : this.wireframeImage?.large);
434
+ const onpressImageUrl = this.visibleOnpress && (isMedium ? this.onpressImage?.medium : this.onpressImage?.large);
388
435
  const getter = isMedium ? getMediumColorImage : getLargeColorImage;
389
- const imageUrl = wireframeImageUrl || getter(this.product, type, this.editableColor, false, ['wireframe']);
436
+ const imageUrl = wireframeImageUrl || onpressImageUrl || getter(this.product, type, this.editableColor, false, ['wireframe']);
390
437
  if (this.backgroundImage && this.backgroundImageUrl === imageUrl) {
391
438
  return this.fabricHelper.setBackgroundImage(this.backgroundImage);
392
439
  }
393
440
 
394
441
  this.backgroundImageLoaded = false;
442
+ this.backgroundImageUrl = imageUrl;
395
443
  fabric.Image.fromURL(imageUrl, image => {
396
- image.scale(this.editorSize.width / image.width);
397
- this.backgroundImage = image;
398
- this.backgroundImageUrl = imageUrl;
399
- this.backgroundImageLoaded = true;
400
- this.fabricHelper.setBackgroundImage(this.backgroundImage);
444
+ if (this.backgroundImageUrl === imageUrl) {
445
+ image.scale(this.editorSize.width / image.width);
446
+ this.backgroundImage = image;
447
+ this.backgroundImageLoaded = true;
448
+ this.fabricHelper.setBackgroundImage(this.backgroundImage);
449
+ }
401
450
  });
402
451
  },
403
452
  removeSelected() {
@@ -215,5 +215,11 @@
215
215
  max-width: 800px;
216
216
  max-height: 800px;
217
217
  }
218
+ &.preview {
219
+ img {
220
+ max-width: 350px;
221
+ max-height: 350px;
222
+ }
223
+ }
218
224
  }
219
225
  }
@@ -1,6 +1,8 @@
1
1
  <template>
2
2
  <div class="Gallery__wrapper">
3
- <div class="Gallery__big">
3
+ <div
4
+ v-if="showBigImage"
5
+ class="Gallery__big">
4
6
  <div class="Gallery__big-image">
5
7
  <img
6
8
  :src="staticLink(currentImage.large)"
@@ -40,7 +42,10 @@
40
42
  </div>
41
43
  <div
42
44
  v-if="isZooming"
43
- class="Gallery__zoom-image">
45
+ class="Gallery__zoom-image"
46
+ :class="{
47
+ preview: !showBigImage
48
+ }">
44
49
  <img
45
50
  :src="staticLink(currentImage.large)"
46
51
  :style="zoomImageStyles"
@@ -54,7 +59,9 @@
54
59
  :class="{
55
60
  'Gallery__small-image--active': (visibleImagesFrom + index) === activeIndex
56
61
  }"
57
- @click="goToSlideAndChangeColor(visibleImagesFrom + index)">
62
+ @click="goToSlideAndChangeColor(visibleImagesFrom + index)"
63
+ @mouseenter="startPreview(visibleImagesFrom + index)"
64
+ @mouseleave="stopZoom">
58
65
  <img
59
66
  :src="staticLink(image.small)"
60
67
  :alt="product.name"
@@ -81,7 +88,7 @@
81
88
  </button>
82
89
  </div>
83
90
  <div
84
- v-if="hasValidFilters"
91
+ v-if="showFilters && hasValidFilters"
85
92
  class="Gallery__filters">
86
93
  <div
87
94
  v-for="filter in validFilters"
@@ -116,6 +123,17 @@ export default {
116
123
  slidesPerRow: {
117
124
  type: Number,
118
125
  default: 6
126
+ },
127
+ showBigImage: {
128
+ type: Boolean,
129
+ default: true
130
+ },
131
+ showFilters: {
132
+ type: Boolean,
133
+ default: true
134
+ },
135
+ imageTypes: {
136
+ type: Array
119
137
  }
120
138
  },
121
139
  data() {
@@ -149,7 +167,7 @@ export default {
149
167
  return this.validFilters.length > 1;
150
168
  },
151
169
  galleryImages() {
152
- return this.images.filter(i => !(i.types || []).includes('designer'));
170
+ return this.imageTypes ? this.images.filter(i => isValidImageTypes(i, this.imageTypes)) : this.images.filter(i => !(i.types || []).includes('designer'));
153
171
  },
154
172
  validFilters() {
155
173
  return this.filters.filter(filter => (this.galleryImages || []).some(i => this.isValidImageByFilter(filter, i)));
@@ -199,6 +217,12 @@ export default {
199
217
  methods: {
200
218
  ...mapActions('product', ['selectColor']),
201
219
  staticLink,
220
+ startPreview(index) {
221
+ if (!this.isZoomin) {
222
+ this.startZoom();
223
+ this.goToSlideAndChangeColor(index);
224
+ }
225
+ },
202
226
  startZoom() {
203
227
  this.isZooming = true;
204
228
  },
@@ -29,7 +29,7 @@
29
29
  class="lc_regular16 ProductMainInfo__description-body"
30
30
  v-html="product.description || '&mdash;'">
31
31
  </p>
32
- <div class="ProductMainInfo__additional-info">
32
+ <!-- <div class="ProductMainInfo__additional-info">
33
33
  <span class="lc_medium16">
34
34
  &bull;
35
35
  Shoulder to shoulder tape
@@ -52,7 +52,7 @@
52
52
  </div>
53
53
  </template>
54
54
  </v-popover>
55
- </div>
55
+ </div> -->
56
56
  </div>
57
57
  </div>
58
58
  </template>
@@ -18,6 +18,17 @@
18
18
  <section class="Product__section">
19
19
  <product-main-info :product="product" />
20
20
  </section>
21
+ <section class="Product__section">
22
+ <btn
23
+ :btn-block="true"
24
+ :to="goToEditorLink"
25
+ btn-class="purple"
26
+ btn-label="DESIGN AND ORDER">
27
+ <i
28
+ slot="icon-after"
29
+ class="icon-arrow-right"></i>
30
+ </btn>
31
+ </section>
21
32
  <section class="Product__section">
22
33
  <wizard-editor />
23
34
  </section>
@@ -44,6 +55,9 @@
44
55
 
45
56
  <script>
46
57
  import { mapGetters } from 'vuex';
58
+ import RelatedProducts from '@lancom/shared/components/product/related_products/related-products';
59
+ import Gallery from '@lancom/shared/components/product/gallery/gallery';
60
+ import { generateProductLink } from '@lancom/shared/assets/js/utils/product';
47
61
  import ProductMainInfo from './layouts/product_main_info/product-main-info';
48
62
  import ProductColorsSelector from './product_colors_selector/product-colors-selector';
49
63
  import ProductTierPrices from './layouts/product_tier_prices/product-tier-prices';
@@ -51,8 +65,6 @@ import ProductFabricAndSizeInfo from './layouts/product_fabric_and_size_info/pro
51
65
  import ProductPackaging from './layouts/product_packaging/product-packaging';
52
66
  import ProductModelMeasurements from './layouts/product_model_measurements/product-model-measurements';
53
67
  import ProductSizeTable from './layouts/product_size_table/product-size-table';
54
- import RelatedProducts from '@lancom/shared/components/product/related_products/related-products';
55
- import Gallery from '@lancom/shared/components/product/gallery/gallery';
56
68
  import WizardEditor from './wizard-editor/wizard-editor.vue';
57
69
 
58
70
 
@@ -80,6 +92,9 @@ export default {
80
92
  'productDetails',
81
93
  'editableColor'
82
94
  ]),
95
+ goToEditorLink() {
96
+ return generateProductLink(this.product, null, true);
97
+ },
83
98
  hasImages() {
84
99
  return this.images.length > 0;
85
100
  }
@@ -6,7 +6,7 @@
6
6
  <div
7
7
  v-if="hasAdditionalColors"
8
8
  :style="{
9
- 'top': additionalColorHeight,
9
+ 'top': hasMainColor ? additionalColorHeight : 0,
10
10
  width: '100%',
11
11
  height: '100%',
12
12
  position: 'relative'
@@ -41,6 +41,9 @@ export default {
41
41
  }
42
42
  },
43
43
  computed: {
44
+ hasMainColor() {
45
+ return (!!this.color?.pattern || !!this.color?.rgb);
46
+ },
44
47
  pattern() {
45
48
  return this.color.pattern && staticLink(this.color.pattern);
46
49
  },
@@ -54,7 +57,9 @@ export default {
54
57
  return (this.color.additionalColors || []);
55
58
  },
56
59
  additionalColorHeight() {
57
- return `${100 / (this.additionalColors.length + 1)}%`;
60
+ const color = this.hasMainColor ? 1 : 0;
61
+ const height = 100 / (this.additionalColors.length + color);
62
+ return `${height}%`;
58
63
  },
59
64
  additionalColorsThumbs() {
60
65
  return (this.color.additionalColors || [])
@@ -23,6 +23,16 @@
23
23
  :has-selected-icon="hasSelectedIcon"
24
24
  :selected-icon-circle="selectedIconCircle" />
25
25
  </div>
26
+ <div class="ProductsAside__item">
27
+ <products-production-time
28
+ :has-selected-icon="hasSelectedIcon"
29
+ :selected-icon-circle="selectedIconCircle" />
30
+ </div>
31
+ <div class="ProductsAside__item">
32
+ <products-minimum-qty
33
+ :has-selected-icon="hasSelectedIcon"
34
+ :selected-icon-circle="selectedIconCircle" />
35
+ </div>
26
36
  <div class="ProductsAside__item">
27
37
  <products-colors />
28
38
  </div>
@@ -44,6 +54,8 @@ import ProductsBrands from '@lancom/shared/components/products/products_brands/p
44
54
  import ProductsTags from '@lancom/shared/components/products/products_tags/products-tags';
45
55
  import ProductsAttributes from '@lancom/shared/components/products/products_attributes/products-attributes';
46
56
  import ProductsColors from '@lancom/shared/components/products/products_colors/products-colors';
57
+ import ProductsProductionTime from '@lancom/shared/components/products/products_production_time/products-production-time';
58
+ import ProductsMinimumQty from '@lancom/shared/components/products/products_minimum_qty/products-minimum-qty';
47
59
  import { generateProductsLink } from '@lancom/shared/assets/js/utils/product';
48
60
 
49
61
  export default {
@@ -53,7 +65,9 @@ export default {
53
65
  ProductsBrands,
54
66
  ProductsTags,
55
67
  ProductsAttributes,
56
- ProductsColors
68
+ ProductsColors,
69
+ ProductsProductionTime,
70
+ ProductsMinimumQty
57
71
  },
58
72
  props: {
59
73
  hasSelectedIcon: {
@@ -27,6 +27,12 @@ export default {
27
27
  color: {
28
28
  type: String
29
29
  },
30
+ productionTime: {
31
+ type: String
32
+ },
33
+ minimumQty: {
34
+ type: String
35
+ },
30
36
  attributes: {
31
37
  type: Object
32
38
  },
@@ -45,6 +51,8 @@ export default {
45
51
  brand: this.brand,
46
52
  tags: this.tags,
47
53
  colors: this.color,
54
+ productionTime: this.productionTime,
55
+ minimumQty: this.minimumQty,
48
56
  option: this.option,
49
57
  text: this.text,
50
58
  sort: this.sort,
@@ -0,0 +1,39 @@
1
+ @import "@/assets/scss/variables";
2
+
3
+ .ProductsMinimumQty {
4
+ &__header {
5
+ font-size: 22px;
6
+ line-height: 20px;
7
+ font-weight: 600;
8
+ margin-top: 3px;
9
+ margin-bottom: 24px;
10
+ color: $black;
11
+ }
12
+ &__item {
13
+ margin: 13px 0;
14
+ font-size: 15px;
15
+ font-weight: 400;
16
+ i {
17
+ font-size: 20px;
18
+ margin-right: 10px;
19
+ }
20
+ a {
21
+ color: $grey_1;
22
+ text-decoration: none;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ }
27
+ &--active a {
28
+ color: $green;
29
+ }
30
+ &-icon {
31
+ margin-right: 10px;
32
+ background-position: center;
33
+ background-repeat: no-repeat;
34
+ background-size: contain;
35
+ width: 20px;
36
+ height: 20px;
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div class="ProductsMinimumQty__wrapper">
3
+ <toggle-content label="Minimum Qty">
4
+ <div
5
+ v-for="minimumQtyRange in minimumQtyRanges"
6
+ :key="minimumQtyRange.alias"
7
+ class="ProductsMinimumQty__item"
8
+ :class="{
9
+ 'ProductsMinimumQty__item--active': isSelectedMinimumQty(minimumQtyRange)
10
+ }">
11
+ <products-link
12
+ class="ProductsMinimumQty__more"
13
+ :minimum-qty="isSelectedMinimumQty(minimumQtyRange) ? null : minimumQtyRange.alias">
14
+ <span>
15
+ {{ minimumQtyRange.name }}
16
+ </span>
17
+ <checked-icon
18
+ v-if="hasSelectedIcon"
19
+ :checked="isSelectedMinimumQty(minimumQtyRange)"
20
+ :circle="selectedIconCircle" />
21
+ </products-link>
22
+ </div>
23
+ </toggle-content>
24
+ </div>
25
+ </template>
26
+
27
+ <script>
28
+ import ToggleContent from '@lancom/shared/components/common/toggle-content';
29
+ import ProductsLink from '@lancom/shared/components/products/products_link/products-link';
30
+ import CheckedIcon from '@lancom/shared/components/common/checked-icon';
31
+
32
+ export default {
33
+ name: 'ProductsMinimumQty',
34
+ components: {
35
+ ProductsLink,
36
+ ToggleContent,
37
+ CheckedIcon
38
+ },
39
+ props: {
40
+ hasSelectedIcon: {
41
+ type: Boolean,
42
+ default: false
43
+ },
44
+ selectedIconCircle: {
45
+ type: Boolean,
46
+ default: false
47
+ }
48
+ },
49
+ data() {
50
+ return {
51
+ minimumQtyRanges: [{
52
+ alias: 'no-minimums',
53
+ name: 'No minimums'
54
+ }, {
55
+ alias: '6-or-less',
56
+ name: '6 or less'
57
+ }, {
58
+ alias: '12-or-less',
59
+ name: '12 or less'
60
+ }, {
61
+ alias: '25-or-less',
62
+ name: '25 or less'
63
+ }, {
64
+ alias: '50-or-less',
65
+ name: '50 or less'
66
+ }, {
67
+ alias: '100-or-less',
68
+ name: '100 or less'
69
+ }]
70
+ };
71
+ },
72
+ computed: {
73
+ selectedMinimumQties() {
74
+ const minimumQty = (this.$route.query.minimumQty || '').split(',');
75
+ return this.minimumQtyRanges.filter(minimumQtyRange => minimumQty.includes(minimumQtyRange.alias));
76
+ }
77
+ },
78
+ methods: {
79
+ isSelectedMinimumQty(minimumQty) {
80
+ return this.selectedMinimumQties.some(mq => mq.alias === minimumQty.alias);
81
+ }
82
+ }
83
+ };
84
+ </script>
85
+
86
+ <style lang="scss" scoped>
87
+ @import 'products-minimum-qty';
88
+ </style>
@@ -0,0 +1,39 @@
1
+ @import "@/assets/scss/variables";
2
+
3
+ .ProductsProductionTime {
4
+ &__header {
5
+ font-size: 22px;
6
+ line-height: 20px;
7
+ font-weight: 600;
8
+ margin-top: 3px;
9
+ margin-bottom: 24px;
10
+ color: $black;
11
+ }
12
+ &__item {
13
+ margin: 13px 0;
14
+ font-size: 15px;
15
+ font-weight: 400;
16
+ i {
17
+ font-size: 20px;
18
+ margin-right: 10px;
19
+ }
20
+ a {
21
+ color: $grey_1;
22
+ text-decoration: none;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ }
27
+ &--active a {
28
+ color: $green;
29
+ }
30
+ &-icon {
31
+ margin-right: 10px;
32
+ background-position: center;
33
+ background-repeat: no-repeat;
34
+ background-size: contain;
35
+ width: 20px;
36
+ height: 20px;
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <div class="ProductsProductionTime__wrapper">
3
+ <toggle-content label="Production Time">
4
+ <div
5
+ v-for="produnctionRange in produnctionTimesRanges"
6
+ :key="produnctionRange.alias"
7
+ class="ProductsProductionTime__item"
8
+ :class="{
9
+ 'ProductsProductionTime__item--active': isSelectedProdunctionTime(produnctionRange)
10
+ }">
11
+ <products-link
12
+ class="ProductsProductionTime__more"
13
+ :production-time="isSelectedProdunctionTime(produnctionRange) ? null : produnctionRange.alias">
14
+ <span>
15
+ {{ produnctionRange.name }}
16
+ </span>
17
+ <checked-icon
18
+ v-if="hasSelectedIcon"
19
+ :checked="isSelectedProdunctionTime(produnctionRange)"
20
+ :circle="selectedIconCircle" />
21
+ </products-link>
22
+ </div>
23
+ </toggle-content>
24
+ </div>
25
+ </template>
26
+
27
+ <script>
28
+ import ToggleContent from '@lancom/shared/components/common/toggle-content';
29
+ import ProductsLink from '@lancom/shared/components/products/products_link/products-link';
30
+ import CheckedIcon from '@lancom/shared/components/common/checked-icon';
31
+
32
+ export default {
33
+ name: 'ProductsProductionTime',
34
+ components: {
35
+ ProductsLink,
36
+ ToggleContent,
37
+ CheckedIcon
38
+ },
39
+ props: {
40
+ hasSelectedIcon: {
41
+ type: Boolean,
42
+ default: false
43
+ },
44
+ selectedIconCircle: {
45
+ type: Boolean,
46
+ default: false
47
+ }
48
+ },
49
+ data() {
50
+ return {
51
+ produnctionTimesRanges: [{
52
+ alias: 'same-day',
53
+ name: 'Same day'
54
+ }, {
55
+ alias: '3-days',
56
+ name: 'Rush 3 day'
57
+ }, {
58
+ alias: '10-days',
59
+ name: '10 days'
60
+ }, {
61
+ alias: '2-weeks',
62
+ name: '2 weeks'
63
+ }, {
64
+ alias: '4-weeks',
65
+ name: '4 or more weeks'
66
+ }]
67
+ };
68
+ },
69
+ computed: {
70
+ selectedProdunctionTimes() {
71
+ const productionTime = (this.$route.query.productionTime || '').split(',');
72
+ return this.produnctionTimesRanges.filter(produnctionTimesRange => productionTime.includes(produnctionTimesRange.alias));
73
+ }
74
+ },
75
+ methods: {
76
+ isSelectedProdunctionTime(produnctionTime) {
77
+ return this.selectedProdunctionTimes.some(pt => pt.alias === produnctionTime.alias);
78
+ }
79
+ }
80
+ };
81
+ </script>
82
+
83
+ <style lang="scss" scoped>
84
+ @import 'products-production-time';
85
+ </style>
@@ -59,8 +59,9 @@ async function googleShoppingFeed(axios, config, availableStores, country, isEdi
59
59
  .replace(/&middot;/, '·');
60
60
 
61
61
  let link = `https://${config.HOST_NAME}${generateProductLink(product, sp.color, isEditor)}`;
62
+ link = link.includes('?') ? `${link}&price=IT` : `${link}?price=IT`
62
63
  if (sp.multipackQty) {
63
- link = `${link}&multipack=${sp.SKU}`
64
+ link = link.includes('?') ? `${link}&multipack=${sp.SKU}` : `${link}?multipack=${sp.SKU}`;
64
65
  }
65
66
 
66
67
  const productWeight = +((product.weight || 0) * (sp.multipackQty || 1)).toFixed(3);
@@ -72,6 +73,7 @@ async function googleShoppingFeed(axios, config, availableStores, country, isEdi
72
73
  'g:item_group_id': { _text: product.SKU },
73
74
  'g:size': { _text: sp.size.name },
74
75
  'g:size_system': COUNTRIES_SIZE_SYSTEMS[country] || country || 'AU',
76
+ 'g:ships_from_country': country || 'AU',
75
77
  'g:size_type': 'regular',
76
78
  'g:gender': { _text: product.gender },
77
79
  'g:material': { _text: product.fabricInfoShort },
@@ -119,6 +121,10 @@ async function googleShoppingFeed(axios, config, availableStores, country, isEdi
119
121
  }
120
122
  }
121
123
 
124
+ if (product.country) {
125
+ info['g:shipping_label'] = product.country;
126
+ }
127
+
122
128
  const productHighlight = `${product.feedProductHighlight || ''}`
123
129
  .trim()
124
130
  .split(/\n+/g)
@@ -61,7 +61,7 @@
61
61
  import gtm from '@lancom/shared/assets/js/utils/gtm';
62
62
  import metaInfo from '@lancom/shared/mixins/meta-info';
63
63
  import { generateProductsLink, generateProductLink } from '@lancom/shared/assets/js/utils/product';
64
- import { getProductLargeCover } from '@lancom/shared/assets/js/utils/colors';
64
+ import { getProductLargeCover, getProductMediumCover } from '@lancom/shared/assets/js/utils/colors';
65
65
  import LazyHydrate from 'vue-lazy-hydration';
66
66
  import TheNavbar from '@/components/the_navbar/the-navbar';
67
67
  import TheChangesSavedIndicator from '@lancom/shared/components/the_changes_saved_indicator/the-changes-saved-indicator';
@@ -93,6 +93,9 @@
93
93
  middleware: ['page-info'],
94
94
  async fetch() {
95
95
  await this.loadProducts();
96
+ if (process.server) {
97
+ this.$root.context?.res?.setHeader('Cache-Control', `max-age=${86400}`);;
98
+ }
96
99
  },
97
100
  computed: {
98
101
  ...mapGetters('page', ['routeInfo']),
@@ -253,7 +256,7 @@
253
256
  };
254
257
  }
255
258
 
256
- const image = getProductLargeCover(product, 'front');
259
+ const image = getProductLargeCover(product, 'front') || getProductMediumCover(product, 'front');
257
260
  if (image) {
258
261
  schema.image = image;
259
262
  }
@@ -324,7 +327,6 @@
324
327
  setTimeout(() => this.logGtm());
325
328
  } catch ({ response }) {
326
329
  if (process.server) {
327
- // console.log('status code: ', this.loadError, process.server, this._self.context, Object.keys(this));
328
330
  this.$root.context.res.statusCode = this.loadError?.statusCode || 500;
329
331
  }
330
332
  }
@@ -1,15 +1,28 @@
1
- export default async function ({ store, route, redirect }) {
1
+ export default async function ({ store, route, redirect, error }) {
2
2
  const matchedRoute = route.matched[route.matched.length - 1];
3
3
  if (matchedRoute) {
4
4
  const path = matchedRoute.path.replace(/\?/g, '');
5
5
  const data = {
6
6
  shop: store.getters.shop._id,
7
7
  route: path || '/',
8
- fullPath: route.fullPath
8
+ fullPath: route.path
9
9
  };
10
10
  const routeInfo = await store.dispatch('page/fetchRouteInfo', data);
11
11
  if (routeInfo?.type === 'redirect' && routeInfo.redirectTo) {
12
- redirect(301, routeInfo.redirectTo);
12
+ redirect(301, route.fullPath.replace(route.path, routeInfo.redirectTo));
13
+ }
14
+ } else {
15
+ const data = {
16
+ shop: store.getters.shop._id,
17
+ route: route.path || '/',
18
+ fullPath: route.path
19
+ };
20
+ const routeInfo = await store.dispatch('page/fetchRouteInfo', data);
21
+ if (routeInfo?.type === 'deleted') {
22
+ error({
23
+ statusCode: 410,
24
+ message: 'Page has been deleted'
25
+ });
13
26
  }
14
27
  }
15
28
  }
@@ -17,7 +17,7 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
17
17
  if (to && to.name === 'brand-tees-slug') { return 'slide-right-to-left'; };
18
18
  return 'fade';
19
19
  },
20
- async asyncData({ store, params, error, query, redirect }) {
20
+ async asyncData({ store, params, error, query, redirect, res }) {
21
21
  try {
22
22
  const { print, color, colour } = query;
23
23
  const { shop, country, stockCountry, currency } = store.getters;
@@ -53,6 +53,9 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
53
53
  if (query.store) {
54
54
  await store.dispatch('product/updatePriceIncludeGST', true);
55
55
  }
56
+ if (query.price) {
57
+ store.commit('product/setPriceIncludeGST', query.price === 'IT');
58
+ }
56
59
  } catch (e) {
57
60
  console.log(e);
58
61
  }
@@ -63,6 +66,8 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
63
66
  error(loadError);
64
67
  }
65
68
 
69
+ res?.setHeader('Cache-Control', `max-age=${3600}`);
70
+
66
71
  return {
67
72
  isEditor,
68
73
  pageItem,
@@ -122,9 +127,10 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
122
127
  };
123
128
  if (!this.product) {
124
129
  await this.fetchProduct(data);
125
- await this.fetchProductDetails(data);
126
130
  }
127
131
 
132
+ await this.fetchProductDetails(data);
133
+
128
134
  try {
129
135
  if (this.preSetPrints?.length) {
130
136
  for (const preSetPrint of this.preSetPrints) {
@@ -187,7 +193,7 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
187
193
  },
188
194
  methods: {
189
195
  ...mapActions(['fetchProduct', 'fetchProductDetails', 'selectColor']),
190
- ...mapMutations(['clearProduct', 'clearTemplate', 'addTemplateLayer', 'setSimpleProductAmount']),
196
+ ...mapMutations(['clearProduct', 'clearTemplate', 'addTemplateLayer', 'setSimpleProductAmount', 'setPriceIncludeGST']),
191
197
  staticLink,
192
198
  goToInfoTab(tab) {
193
199
  this.$refs.infoTabs.selectedTab = tab;
@@ -235,7 +241,7 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
235
241
  const productSchema = {
236
242
  '@context': 'https://schema.org',
237
243
  '@type': 'Product',
238
- description: this.product.gsFeedDescription || this.product.description,
244
+ description: (this.product.gsFeedDescription || this.product.description || '').replace(/<[^>]*>/g, ''),
239
245
  name,
240
246
  offers: this.productDetails?.simpleProducts.map(sp => {
241
247
  const spMaxPrice = (sp.pricing || []).reduce((price, pricing) => Math.max(price, pricing.price), 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lancom/shared",
3
- "version": "0.0.351",
3
+ "version": "0.0.353",
4
4
  "description": "lancom common scripts",
5
5
  "author": "e.tokovenko <e.tokovenko@gmail.com>",
6
6
  "repository": {
@@ -1,13 +1,17 @@
1
1
  module.exports = function (req, res, next) {
2
+ if (res.getHeader('Cache-Control')) {
3
+ return next();
4
+ }
5
+
2
6
  if (!/auth\=/.test(req.headers?.cookie || '')) {
3
7
  const routes = [
4
8
  /^\/quotes\//,
5
9
  /\.xml$/
6
10
  ];
7
11
  const value = routes.some(route => route.test(`${req.url}`)) ? 'no-cache' : `max-age=${31536000}`;
8
- res.setHeader('Cache-Control', value);
12
+ res?.setHeader('Cache-Control', value);
9
13
  } else {
10
- res.setHeader('Cache-Control', 'no-cache');
14
+ res?.setHeader('Cache-Control', 'no-cache');
11
15
  }
12
16
  next();
13
17
  };
package/store/product.js CHANGED
@@ -12,6 +12,7 @@ import { sortByName } from '../assets/js/utils/filters';
12
12
  export const state = () => ({
13
13
  multipack: null,
14
14
  calculatingPrice: false,
15
+ loadingProductDetails: false,
15
16
  images: [],
16
17
  priceIncludeGST: false,
17
18
  product: null,
@@ -34,6 +35,7 @@ export const state = () => ({
34
35
  isPrintPricing: false,
35
36
  editableColor: null,
36
37
  selectedLayer: null,
38
+ selectedLayerCopy: null,
37
39
  editablePrintArea: null,
38
40
  editablePrintAreas: [],
39
41
  selectedPrintType: null,
@@ -49,6 +51,7 @@ export const state = () => ({
49
51
  });
50
52
 
51
53
  export const getters = {
54
+ loadingProductDetails: ({ loadingProductDetails }) => loadingProductDetails,
52
55
  multipack: ({ multipack }) => multipack,
53
56
  calculatingPrice: ({ calculatingPrice }) => calculatingPrice,
54
57
  product: ({ product }) => product,
@@ -78,6 +81,7 @@ export const getters = {
78
81
  editableColor: ({ editableColor }) => editableColor,
79
82
  editableLayers: ({ template, editableColor, editablePrintArea }) => template.layers,
80
83
  selectedLayer: ({ selectedLayer }) => selectedLayer,
84
+ selectedLayerCopy: ({ selectedLayerCopy }) => selectedLayerCopy,
81
85
  editModeSelectedLayer: ({ editModeSelectedLayer }) => editModeSelectedLayer,
82
86
  selectedPrintAreas: ({ selectedPrintAreas }) => selectedPrintAreas,
83
87
  selectedPrintArea: ({ selectedPrintAreas, editablePrintArea }) =>
@@ -177,8 +181,10 @@ export const actions = {
177
181
  },
178
182
  async fetchProductDetails({ commit, state }, { shop, slug, country, stockCountry, currency, defaultColor = 'white' }) {
179
183
  const params = { country, currency, stockCountry };
184
+ commit('setLoadingProductDetails', true);
180
185
  const response = await api.fetchProductDetails(shop, slug, params);
181
186
  commit('setProductDetails', response);
187
+ commit('setLoadingProductDetails', false);
182
188
 
183
189
  const catalogFrontColorId = getColorOfDefaultCatalogFrontImage(state.product);
184
190
  const editableColor = state.availableColors.find(c => c.alias === defaultColor) ||
@@ -273,6 +279,9 @@ export const actions = {
273
279
  };
274
280
 
275
281
  export const mutations = {
282
+ setLoadingProductDetails(state, loadingProductDetails) {
283
+ state.loadingProductDetails = loadingProductDetails;
284
+ },
276
285
  setMultipack(state, multipack) {
277
286
  state.multipack = multipack;
278
287
  },
@@ -351,6 +360,7 @@ export const mutations = {
351
360
  */
352
361
  setSelectedLayer(state, layer) {
353
362
  state.selectedLayer = layer;
363
+ state.selectedLayerCopy = JSON.parse(JSON.stringify(layer || null));
354
364
  },
355
365
  setEditModeSelectedLayer(state, editMode) {
356
366
  state.editModeSelectedLayer = editMode;