@lancom/shared 0.0.469 → 0.0.471

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 (27) hide show
  1. package/assets/js/api/admin.js +6 -0
  2. package/assets/js/api/index.js +5 -2
  3. package/assets/js/utils/fabric-helper.js +6 -4
  4. package/assets/js/utils/simple-products.js +4 -1
  5. package/assets/scss/ui_kit/_grid.scss +2 -2
  6. package/components/checkout/cart/cart.vue +28 -0
  7. package/components/common/checkbox.vue +1 -1
  8. package/components/editor/editor_print_area_options/editor-print-area-options.scss +23 -0
  9. package/components/editor/editor_print_area_options/editor-print-area-options.vue +35 -16
  10. package/components/editor/editor_print_area_options/editor_print_area_option/editor-print-area-option.scss +0 -20
  11. package/components/editor/editor_print_area_options/editor_print_area_option/editor-print-area-option.vue +11 -21
  12. package/components/editor/editor_print_area_options/editor_print_area_option/editor_print_area_suboption/editor-print-area-suboption.scss +0 -0
  13. package/components/editor/editor_print_area_options/editor_print_area_option/editor_print_area_suboption/editor-print-area-suboption.vue +86 -0
  14. package/components/editor/editor_product_details/editor-product-details.vue +5 -1
  15. package/components/editor/editor_workspace/editor-workspace.vue +7 -1
  16. package/components/editor/editor_workspace/editor_workspace_side/editor-workspace-side.vue +10 -3
  17. package/components/product/simple_products_price_info/simple-products-price-info.vue +5 -1
  18. package/components/products/products_autocomplete/products-autocomplete.vue +1 -1
  19. package/components/quotes/quote_request/quote-request.vue +22 -13
  20. package/components/quotes/quote_request_modal/quote-request-modal.scss +14 -2
  21. package/components/quotes/quote_request_modal/quote-request-modal.vue +23 -2
  22. package/mixins/layouts/products.js +55 -42
  23. package/mixins/product-view.js +5 -1
  24. package/nuxt.config.js +39 -7
  25. package/package.json +1 -1
  26. package/server-middleware/sitemap.js +37 -0
  27. package/store/products.js +3 -3
@@ -852,4 +852,10 @@ export default {
852
852
  getReOrderReport(params) {
853
853
  return _get(`admin/reports/re-order`, params);
854
854
  },
855
+ fetchOrderD14(orderId) {
856
+ return _get(`admin/order/${orderId}/d14`);
857
+ },
858
+ fetchPurchaseOrderD14(purchaseOrderId) {
859
+ return _get(`admin/purchase-order/${purchaseOrderId}/d14`);
860
+ },
855
861
  };
@@ -42,8 +42,8 @@ const api = {
42
42
  fetchCustomerCoupons(customer, shop) {
43
43
  return _get(`shop/${shop}/customer/${customer._id}/coupons`);
44
44
  },
45
- fetchChildrenCategories(category, params) {
46
- return _get(`category/${category || 'null'}/children`, params);
45
+ fetchChildrenCategories(category, shop, params) {
46
+ return _get(`shop/${shop}/category/${category || 'null'}/children`, params);
47
47
  },
48
48
  searchProducts(shop, text, save) {
49
49
  return _get(`shop/${shop}/products/search`, { text, save });
@@ -150,6 +150,9 @@ const api = {
150
150
  saveQuoteRequest(quote, shop) {
151
151
  return quote._id ? _put(`shop/${shop}/quotes/${quote._id}`, quote) : _post(`shop/${shop}/quotes`, quote);
152
152
  },
153
+ saveCartQuoteRequest(data, shop) {
154
+ return _post(`shop/${shop}/quotes/cart`, data);
155
+ },
153
156
  generateQuotePDF(id, option) {
154
157
  return _get(`quotes/${id}/option/${option}/pdf`);
155
158
  },
@@ -13,8 +13,9 @@ class Background {
13
13
  boundinStrokeColor = '#7D6AEF';
14
14
  backgroundImage = null;
15
15
 
16
- constructor({ background, size }) {
16
+ constructor({ background, size, hideBoundBox = false }) {
17
17
  this.canvas = new fabric.Canvas(background, { ...size });
18
+ this.hideBoundBox = hideBoundBox;
18
19
  }
19
20
 
20
21
  clear() {
@@ -28,7 +29,7 @@ class Background {
28
29
  }
29
30
 
30
31
  addBoundingRect() {
31
- if (this.boundingRect) {
32
+ if (this.boundingRect && !this.hideBoundBox) {
32
33
  this.boundingBox = new fabric.Rect({
33
34
  ...this.boundingRect,
34
35
  hasBorders: false,
@@ -66,11 +67,12 @@ export default class FabricHelper {
66
67
  background = null;
67
68
  printAreaRect = null;
68
69
 
69
- constructor({ editor, background, size }) {
70
+ constructor({ editor, background, size, hideBoundBox = false }) {
70
71
  this.size = size;
72
+ this.hideBoundBox = hideBoundBox;
71
73
  this.editor = new fabric.Canvas(editor, { ...size });
72
74
  if (background) {
73
- this.background = new Background({ background, size });
75
+ this.background = new Background({ background, size, hideBoundBox });
74
76
  }
75
77
  this.events = {};
76
78
  this.boundingBox = null;
@@ -1,3 +1,5 @@
1
+ import { sortSizes } from './sizes';
2
+
1
3
  export function getSimpleProductsColors(simpleProducts, onlyWithStock = true) {
2
4
  const uniqueColorsMap = new Map();
3
5
  (simpleProducts || [])
@@ -20,5 +22,6 @@ export function getSimpleProductsSizes(simpleProducts, sizesColor, onlyWithStock
20
22
  uniqueSizesMap.set(size._id, size);
21
23
  }
22
24
  });
23
- return Array.from(uniqueSizesMap.values());
25
+ const sizes = Array.from(uniqueSizesMap.values());
26
+ return sortSizes(sizes);
24
27
  }
@@ -783,7 +783,7 @@
783
783
  margin-left: auto !important;
784
784
  }
785
785
  // margins
786
- @for $i from 1 through 20 {
786
+ @for $i from 0 through 20 {
787
787
  .mt-#{$i} {
788
788
  margin-top: #{$i}px !important;
789
789
  }
@@ -796,4 +796,4 @@
796
796
  .ml-#{$i} {
797
797
  margin-left: #{$i}px !important;
798
798
  }
799
- }
799
+ }
@@ -63,6 +63,13 @@
63
63
  slot="icon-after"
64
64
  class="icon-arrow-right"></i>
65
65
  </btn>
66
+ <btn
67
+ class="mt-10 ml-0"
68
+ :btn-disabled="isSaveQuoteDisabled"
69
+ :btn-block="true"
70
+ btn-class="white"
71
+ btn-label="SAVE AS QUOTE FOR LATER"
72
+ @onclick="openSaveQuoteModal" />
66
73
  </div>
67
74
  <div class="mt-10 hidden-sm-and-up">
68
75
  <btn
@@ -94,6 +101,7 @@ import CartMixin from '@lancom/shared/components/checkout/cart/cart.mixin';
94
101
  import CartPriceInfo from '@lancom/shared/components/checkout/cart/cart_price_info/cart-price-info';
95
102
  import CartShipping from '@lancom/shared/components/checkout/cart/cart_shipping/cart-shipping';
96
103
  import Checkbox from '@lancom/shared/components/common/checkbox';
104
+ import QuoteRequestModal from '@lancom/shared/components/quotes/quote_request_modal/quote-request-modal';
97
105
 
98
106
  export default {
99
107
  name: 'CheckoutCart',
@@ -147,11 +155,31 @@ export default {
147
155
  },
148
156
  isCheckoutDisabled() {
149
157
  return this.isNotValidShipping || this.isNotValidQuantity || this.cartPricingError || this.cartPricingCalculating || (this.hasClearanceProduct && !this.clearanceAccepted);
158
+ },
159
+ isSaveQuoteDisabled() {
160
+ return this.isEmpty || this.isNotValidQuantity || this.cartPricingError || this.cartPricingCalculating;
150
161
  }
151
162
  },
152
163
  methods: {
153
164
  loadedPricing() {
154
165
  gtm.viewCart(this.entitiesWithoutFreeProducts, this.cartPricing, this.currency);
166
+ },
167
+ openSaveQuoteModal() {
168
+ const params = {
169
+ name: 'cart-quote-request',
170
+ root: this.$root,
171
+ height: 'auto',
172
+ width: 1012,
173
+ adaptive: true,
174
+ clickToClose: true,
175
+ transition: 'from-right-to-left'
176
+ };
177
+ this.$modal.show(QuoteRequestModal, {
178
+ visibleProductTypes: false,
179
+ hideQuoteExtras: true,
180
+ title: 'Save cart as quote',
181
+ intro: 'Complete your details below. We will email you a link to your quote when it is ready.'
182
+ }, params);
155
183
  }
156
184
  }
157
185
  };
@@ -127,7 +127,7 @@ html[dir=rtl] {
127
127
  top: 50%;
128
128
  width: 24px;
129
129
  height: 24px;
130
- border: 1px solid $grey_3;
130
+ border: 1px solid #616277;
131
131
  border-radius: 2px;
132
132
  transform: translateY(-50%);
133
133
  }
@@ -26,5 +26,28 @@
26
26
  grid-column: 1/3;
27
27
  grid-row: 3/3;
28
28
  }
29
+ ::v-deep .EditorPrintAreaOption {
30
+ &__wrapper {
31
+ position: relative;
32
+ width: 100%;
33
+ height: 100%;
34
+ }
35
+ &__content {
36
+ text-align: center;
37
+ padding: 8px;
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ &:hover {
42
+ background-color: $green_lighter;
43
+ }
44
+ &.selected {
45
+ background-color: $green;
46
+ }
47
+ > * {
48
+ pointer-events: none;
49
+ }
50
+ }
51
+ }
29
52
  }
30
53
  }
@@ -2,31 +2,50 @@
2
2
  <div
3
3
  class="EditorPrintAreaOptions__wrapper"
4
4
  :class="{ singleRow, allowNoPrint }">
5
- <div
6
- v-for="(option, index) in options"
7
- :key="index"
8
- class="EditorPrintAreaOptions__option"
9
- :class="{
10
- 'EditorPrintAreaOptions__option--no-print': !option.printArea
11
- }">
12
- <editor-print-area-option
13
- :option="option"
14
- :product="product"
15
- :selected="option.printArea && option.printArea._id === selected"
16
- @select="select"
17
- @option-mouseover="$emit('option-mouseover', $event)"
18
- @option-mouseleave="$emit('option-mouseleave', $event)" />
19
- </div>
5
+ <template v-if="options.length === 1">
6
+ <div
7
+ v-for="(suboption, index) in options[0].suboptions"
8
+ :key="index"
9
+ class="EditorPrintAreaOptions__option">
10
+ <editor-print-area-suboption
11
+ :suboption="suboption"
12
+ :product="product"
13
+ :side="options[0].printArea.side"
14
+ :selected="suboption.printArea && suboption.printArea._id === selected"
15
+ @select="select"
16
+ @option-mouseover="$emit('option-mouseover', $event)"
17
+ @option-mouseleave="$emit('option-mouseleave', $event)" />
18
+ </div>
19
+ </template>
20
+ <template v-else>
21
+ <div
22
+ v-for="(option, index) in options"
23
+ :key="index"
24
+ class="EditorPrintAreaOptions__option"
25
+ :class="{
26
+ 'EditorPrintAreaOptions__option--no-print': !option.printArea
27
+ }">
28
+ <editor-print-area-option
29
+ :option="option"
30
+ :product="product"
31
+ :selected="option.printArea && option.printArea._id === selected"
32
+ @select="select"
33
+ @option-mouseover="$emit('option-mouseover', $event)"
34
+ @option-mouseleave="$emit('option-mouseleave', $event)" />
35
+ </div>
36
+ </template>
20
37
  </div>
21
38
  </template>
22
39
 
23
40
  <script>
24
41
  import EditorPrintAreaOption from './editor_print_area_option/editor-print-area-option';
42
+ import EditorPrintAreaSuboption from './editor_print_area_option/editor_print_area_suboption/editor-print-area-suboption';
25
43
 
26
44
  export default {
27
45
  name: 'EditorPrintAreaOptions',
28
46
  components: {
29
- EditorPrintAreaOption
47
+ EditorPrintAreaOption,
48
+ EditorPrintAreaSuboption
30
49
  },
31
50
  props: {
32
51
  selected: {
@@ -1,26 +1,6 @@
1
1
  @import "@/assets/scss/variables";
2
2
 
3
3
  .EditorPrintAreaOption {
4
- &__wrapper {
5
- position: relative;
6
- width: 100%;
7
- height: 100%;
8
- }
9
- &__content {
10
- text-align: center;
11
- padding: 8px 8px 8px 0;
12
- display: flex;
13
- align-items: center;
14
- &:hover {
15
- background-color: $green_lighter;
16
- }
17
- &.selected {
18
- background-color: $green;
19
- }
20
- > * {
21
- pointer-events: none;
22
- }
23
- }
24
4
  &__info {
25
5
  text-align: center;
26
6
  display: flex;
@@ -35,28 +35,16 @@
35
35
  v-if="showOptions"
36
36
  v-click-outside="closeSuboptions"
37
37
  class="EditorPrintAreaOption__suboptions">
38
- <div
38
+ <editor-print-area-suboption
39
39
  v-for="(suboption, index) in option.suboptions"
40
40
  :key="index"
41
- class="EditorPrintAreaOption__content"
42
- @click="selectSuboption(suboption)"
43
- @mouseover="$emit('option-mouseover', suboption)"
44
- @mouseleave="$emit('option-mouseleave', suboption)">
45
- <div
46
- v-if="suboption.printArea"
47
- class="EditorPrintAreaOption__icon"
48
- :class="{
49
- [suboption.printArea.alias]: true,
50
- [option.printArea.side]: true,
51
- [productType]: true
52
- }">
53
- </div>
54
- <div>
55
- <div class="lc_caption">
56
- {{ suboption.label }}. {{ suboption.sizeLabel }}
57
- </div>
58
- </div>
59
- </div>
41
+ :suboption="suboption"
42
+ :product="product"
43
+ :side="option.printArea.side"
44
+ :product-type="productType"
45
+ @select="selectSuboption"
46
+ @option-mouseover="$emit('option-mouseover', $event)"
47
+ @option-mouseleave="$emit('option-mouseleave', $event)" />
60
48
  </div>
61
49
  </div>
62
50
  </template>
@@ -66,11 +54,13 @@ import { mapGetters } from 'vuex';
66
54
  import { priceWithTax, priceInRange } from '@lancom/shared/assets/js/utils/filters';
67
55
  import { getProductPrintsAreasPrices } from '@lancom/shared/assets/js/models/print-area';
68
56
  import Price from '@lancom/shared/components/common/price';
57
+ import EditorPrintAreaSuboption from './editor_print_area_suboption/editor-print-area-suboption';
69
58
 
70
59
  export default {
71
60
  name: 'EditorPrintAreaOption',
72
61
  components: {
73
- Price
62
+ Price,
63
+ EditorPrintAreaSuboption
74
64
  },
75
65
  filters: {
76
66
  priceWithTax
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <div class="EditorPrintAreaOption__wrapper">
3
+ <div
4
+ class="EditorPrintAreaOption__content"
5
+ :class="{ selected }"
6
+ @click="$emit('select', suboption)"
7
+ @mouseover="$emit('option-mouseover', suboption)"
8
+ @mouseleave="$emit('option-mouseleave', suboption)">
9
+ <div
10
+ v-if="suboption.printArea"
11
+ class="EditorPrintAreaOption__icon"
12
+ :class="{
13
+ [suboption.printArea.alias]: true,
14
+ [side]: true,
15
+ [productType]: true
16
+ }">
17
+ </div>
18
+ <div class="EditorPrintAreaOption__info">
19
+ <div class="lc_caption">
20
+ {{ suboption.label }}. {{ suboption.sizeLabel }}
21
+ </div>
22
+ <div class="lc_regular10">
23
+ <price
24
+ :price="calcPrintPrice(suboption)"
25
+ :with-gst="priceIncludeGST" />
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script>
33
+ import { mapGetters } from 'vuex';
34
+ import { priceInRange } from '@lancom/shared/assets/js/utils/filters';
35
+ import { getProductPrintsAreasPrices } from '@lancom/shared/assets/js/models/print-area';
36
+ import Price from '@lancom/shared/components/common/price';
37
+
38
+ export default {
39
+ name: 'EditorPrintAreaSuboption',
40
+ components: {
41
+ Price
42
+ },
43
+ props: {
44
+ suboption: {
45
+ type: Object,
46
+ required: true
47
+ },
48
+ product: {
49
+ type: Object,
50
+ required: true
51
+ },
52
+ side: {
53
+ type: String,
54
+ required: true
55
+ },
56
+ productType: {
57
+ type: String,
58
+ default: 'tee'
59
+ },
60
+ selected: {
61
+ type: Boolean,
62
+ default: false
63
+ }
64
+ },
65
+ computed: {
66
+ ...mapGetters('product', [
67
+ 'usedSimpleProducts',
68
+ 'selectedPrintType',
69
+ 'priceIncludeGST'
70
+ ]),
71
+ printsPricing() {
72
+ return getProductPrintsAreasPrices(this.product, this.selectedPrintType);
73
+ }
74
+ },
75
+ methods: {
76
+ calcPrintPrice(suboption) {
77
+ const amount = this.usedSimpleProducts.reduce((amount, product) => product.amount + amount, 0);
78
+ return suboption.printArea ? priceInRange(this.printsPricing[suboption.printArea._id], amount || 1) : null;
79
+ }
80
+ }
81
+ };
82
+ </script>
83
+
84
+ <style lang="scss" scoped>
85
+ @import 'editor-print-area-suboption';
86
+ </style>
@@ -284,7 +284,11 @@ export default {
284
284
  clickToClose: true,
285
285
  transition: 'from-right-to-left'
286
286
  };
287
- this.$modal.show(QuoteRequestModal, { visibleProductTypes: false }, params);
287
+ this.$modal.show(QuoteRequestModal, {
288
+ visibleProductTypes: false,
289
+ title: 'Request a quote',
290
+ intro: 'Tell us what you need for this product. We will email you within 24-48 hours.'
291
+ }, params);
288
292
  },
289
293
  toggleSide() {
290
294
  this.side = this.side === 'front' ? 'back' : 'front';
@@ -36,6 +36,7 @@
36
36
  :zoom-size="sideZoomSize"
37
37
  :print-area-size="selectedPrintArea"
38
38
  :visible-background-image="visibleBackgroundImage"
39
+ :hide-bound-box="hideBoundBox"
39
40
  :class="side"
40
41
  class="EditorWorkspace__side rotate-y-element"
41
42
  @workspace="onWorkspaceChange($event)"
@@ -75,6 +76,7 @@
75
76
  :class="editableSide.id"
76
77
  :zoom-size="sideZoomSize"
77
78
  :visible-background-image="visibleBackgroundImage"
79
+ :hide-bound-box="hideBoundBox"
78
80
  class="EditorWorkspace__side rotate-y-element"
79
81
  @workspace="onWorkspaceChange($event)"
80
82
  @print-area-changed="onPrintAreaChanged($event)" />
@@ -165,6 +167,10 @@ export default {
165
167
  isSelectable: {
166
168
  type: Boolean,
167
169
  default: true
170
+ },
171
+ hideBoundBox: {
172
+ type: Boolean,
173
+ default: false
168
174
  }
169
175
  },
170
176
  data() {
@@ -306,7 +312,7 @@ export default {
306
312
  this.setSelectedPrintArea({ sideId: this.editableSide.id, printArea: printArea?._id, parentPrintArea: printArea?.parentPrintArea, size });
307
313
  },
308
314
  toogleBoundBox(state, printAreaOption) {
309
- if (this.$refs.editor) {
315
+ if (this.$refs.editor && !this.hideBoundBox) {
310
316
  this.$refs.editor.toogleBoundBox(state, printAreaOption);
311
317
  }
312
318
  },
@@ -175,6 +175,10 @@ export default {
175
175
  isSelectable: {
176
176
  type: Boolean,
177
177
  default: true
178
+ },
179
+ hideBoundBox: {
180
+ type: Boolean,
181
+ default: false
178
182
  }
179
183
  },
180
184
  data() {
@@ -389,7 +393,8 @@ export default {
389
393
  this.fabricHelper = new FabricHelper({
390
394
  editor: this.$refs.editor,
391
395
  background: this.$refs.background,
392
- size: this.editorSize
396
+ size: this.editorSize,
397
+ hideBoundBox: this.hideBoundBox
393
398
  });
394
399
  this.fabricHelper.setPrintArea(this.printArea, this.editorSize, this.product);
395
400
  this.fabricHelper.on('setField', data => {
@@ -422,7 +427,9 @@ export default {
422
427
  }
423
428
  this.drawingInProcess = true;
424
429
  this.fabricHelper.clear();
425
- this.fabricHelper.addBoundingArea();
430
+ if (!this.hideBoundBox) {
431
+ this.fabricHelper.addBoundingArea();
432
+ }
426
433
  await this.addLayersToCanvas();
427
434
  this.drawingInProcess = false;
428
435
 
@@ -559,7 +566,7 @@ export default {
559
566
  );
560
567
  },
561
568
  toogleBoundBox(state, option) {
562
- if (!this.fabricHelper || ['mini', 'xs'].includes(this.breakpoints.is)) {
569
+ if (!this.fabricHelper || ['mini', 'xs'].includes(this.breakpoints.is) || this.hideBoundBox) {
563
570
  return;
564
571
  }
565
572
 
@@ -10,6 +10,7 @@
10
10
  <script>
11
11
  import { mapGetters } from 'vuex';
12
12
  import PricingTable from '@lancom/shared/components/common/pricing_table/pricing-table';
13
+ import { sortSizes } from '@lancom/shared/assets/js/utils/sizes';
13
14
 
14
15
  export default {
15
16
  name: 'ProductSizesInfo',
@@ -25,13 +26,16 @@ export default {
25
26
  computed: {
26
27
  ...mapGetters('product', ['priceIncludeGST']),
27
28
  sizesPrices() {
28
- return [...(this.simpleProducts || []).reduce((sizes, product) => {
29
+ const list = [...(this.simpleProducts || []).reduce((sizes, product) => {
29
30
  sizes.set(product.size._id, {
30
31
  name: product.size.shortName,
32
+ order: product.size.order,
31
33
  pricing: product.pricing
32
34
  });
33
35
  return sizes;
34
36
  }, new Map()).values()];
37
+
38
+ return sortSizes(list);
35
39
  }
36
40
  }
37
41
  };
@@ -83,7 +83,7 @@ export default {
83
83
  visibleResult: false,
84
84
  text: '',
85
85
  products: [],
86
- searchWithDebounce: debounce(this.search, 500),
86
+ searchWithDebounce: debounce(this.search, 1000),
87
87
  hideTimer: null
88
88
  };
89
89
  },
@@ -63,7 +63,9 @@
63
63
  {{ errors[0] }}
64
64
  </span>
65
65
  </validation-provider>
66
- <div class="form-row">
66
+ <div
67
+ v-if="!hideQuoteExtras"
68
+ class="form-row">
67
69
  <div class="QuoteRequest__label">
68
70
  Upload your logo/artwork
69
71
  </div>
@@ -121,6 +123,7 @@
121
123
  </div>
122
124
  </div>
123
125
  <validation-provider
126
+ v-if="!hideQuoteExtras"
124
127
  v-slot="{ errors }"
125
128
  tag="div"
126
129
  name="Description"
@@ -210,7 +213,9 @@
210
213
  {{ errors[0] }}
211
214
  </span>
212
215
  </validation-provider>
213
- <div class="form-row">
216
+ <div
217
+ v-if="!hideQuoteExtras"
218
+ class="form-row">
214
219
  <div class="QuoteRequest__label">
215
220
  Do these need to be printed?
216
221
  </div>
@@ -307,6 +312,16 @@ export default {
307
312
  Multiselect,
308
313
  PhoneInput
309
314
  },
315
+ props: {
316
+ visibleProductTypes: {
317
+ type: Boolean,
318
+ default: true
319
+ },
320
+ hideQuoteExtras: {
321
+ type: Boolean,
322
+ default: false
323
+ }
324
+ },
310
325
  data() {
311
326
  return {
312
327
  processing: false,
@@ -360,15 +375,9 @@ export default {
360
375
  }
361
376
  };
362
377
  },
363
- props: {
364
- visibleProductTypes: {
365
- type: Boolean,
366
- default: true
367
- }
368
- },
369
378
  computed: {
370
379
  ...mapGetters(['shop', 'currency', 'googleClickId']),
371
- ...mapGetters('cart', ['entities']),
380
+ ...mapGetters('cart', ['entities', 'entitiesWithoutFreeProducts'])
372
381
  },
373
382
  methods: {
374
383
  handleUploaded(file) {
@@ -408,7 +417,7 @@ export default {
408
417
  googleClickId: this.googleClickId,
409
418
  referer: window.location.href,
410
419
  cartInfo: {
411
- entities: (this.entities || [])
420
+ entities: (this.entitiesWithoutFreeProducts || this.entities || [])
412
421
  .map(({ product, simpleProducts, prints }) => {
413
422
  return {
414
423
  product: product ? { _id: product.name, name: product.name, SKU: product.SKU } : null,
@@ -416,14 +425,14 @@ export default {
416
425
  .map(({ layers, printArea, printSize, printType }) => ({
417
426
  layers,
418
427
  printArea: printArea ? { _id: printArea._id, name: printArea.name } : null,
419
- printSize: printSize ? { _id: printSize._id, name: printSize.name} : null,
420
- printType: printType ? { _id: printType._id, name: printType.name} : null,
428
+ printSize: printSize ? { _id: printSize._id, name: printSize.name } : null,
429
+ printType: printType ? { _id: printType._id, name: printType.name } : null
421
430
  })),
422
431
  simpleProducts: (simpleProducts || [])
423
432
  .filter(({ amount }) => amount > 0)
424
433
  .map(({ amount, SKU }) => ({
425
434
  amount,
426
- SKU,
435
+ SKU
427
436
  }))
428
437
  };
429
438
  })
@@ -1,16 +1,28 @@
1
1
  @import "@/assets/scss/variables";
2
2
 
3
3
  .QuoteRequestModal {
4
- &__wrapper {
4
+ &__wrapper {
5
5
  border-radius: 0px;
6
6
  margin-bottom: 50px;
7
+ max-width: 100%;
8
+
7
9
  .lc_modal__content {
8
10
  @media (min-width: $bp-extra-small-max) {
9
11
  padding: 0 0 20px 0 !important;
10
12
  }
11
13
  }
14
+
12
15
  .icon-cancel {
13
16
  z-index: 2;
14
17
  }
15
18
  }
16
- }
19
+
20
+ &__title {
21
+ font-size: 20px;
22
+ font-weight: bold;
23
+ }
24
+ &__intro {
25
+ padding: 0 20px;
26
+ margin-bottom: 20px;
27
+ }
28
+ }
@@ -1,13 +1,24 @@
1
1
  <template>
2
2
  <div class="lc_modal__wrapper QuoteRequestModal__wrapper">
3
3
  <div class="lc_modal__header lc_modal__header--small-padding pull-left">
4
+ <h5
5
+ v-if="title"
6
+ class="lc_modal__header-title QuoteRequestModal__title">
7
+ {{ title }}
8
+ </h5>
4
9
  <i
5
10
  class="icon-cancel lc_modal__close--dark"
6
11
  @click="$emit('close')"></i>
7
12
  </div>
8
- <div class="lc_modal__content">
13
+ <div class="lc_modal__content QuoteRequestModal__content">
14
+ <p
15
+ v-if="intro"
16
+ class="lc_regular14 lc__grey1 lc_regular12 QuoteRequestModal__intro">
17
+ {{ intro }}
18
+ </p>
9
19
  <quote-request
10
20
  :visible-product-types="visibleProductTypes"
21
+ :hide-quote-extras="hideQuoteExtras"
11
22
  @added="$emit('close', $event)" />
12
23
  </div>
13
24
  </div>
@@ -25,8 +36,18 @@ export default {
25
36
  visibleProductTypes: {
26
37
  type: Boolean,
27
38
  default: true
39
+ },
40
+ title: {
41
+ type: String
42
+ },
43
+ intro: {
44
+ type: String
45
+ },
46
+ hideQuoteExtras: {
47
+ type: Boolean,
48
+ default: false
28
49
  }
29
- },
50
+ }
30
51
  };
31
52
  </script>
32
53
 
@@ -8,15 +8,36 @@ import { getProductLargeCover, getProductMediumCover } from '@lancom/shared/asse
8
8
  export default {
9
9
  mixins: [metaInfo],
10
10
  middleware: ['page-info'],
11
- async fetch() {
12
- await this.loadProducts();
11
+ async asyncData({ store, route, res }) {
12
+ const shop = store.getters.shop;
13
+ const country = store.getters.country;
14
+ const currency = store.getters.currency;
13
15
 
14
- if (this.page === 1) {
15
- await this.fetchChildrenCategories(this.currentCategory);
16
+ const params = {
17
+ ...route.params,
18
+ ...route.query,
19
+ country: country?._id,
20
+ currency: currency?._id,
21
+ limit: 80
22
+ };
23
+
24
+ try {
25
+ await store.dispatch('products/fetchProducts', { params, shop: shop._id });
26
+ } catch {
27
+ if (process.server && res) {
28
+ const loadError = store.getters['products/loadError'];
29
+ res.statusCode = loadError?.statusCode || 500;
30
+ }
16
31
  }
17
32
 
18
- if (process.server) {
19
- this.$root.context?.res?.setHeader('Cache-Control', `max-age=${86400}`);;
33
+ const page = store.getters['products/page'];
34
+ if (page === 1) {
35
+ const category = store.getters['products/category'];
36
+ await store.dispatch('products/fetchChildrenCategories', { category, shop });
37
+ }
38
+
39
+ if (process.server && res) {
40
+ res.setHeader('Cache-Control', `max-age=${86400}`);
20
41
  }
21
42
  },
22
43
  computed: {
@@ -53,29 +74,9 @@ export default {
53
74
  return this.category;
54
75
  },
55
76
  breadcrumbs() {
56
- const breadcrumbs = [{
57
- to: '/products',
58
- text: 'Products'
59
- }];
60
- if (this.currentCategory) {
61
- const categoriesBreadcrumbs = [];
62
- let category = this.currentCategory;
63
- while (category) {
64
- categoriesBreadcrumbs.unshift({
65
- to: generateProductsLink(this.$route, {
66
- type: null,
67
- brand: null,
68
- tag: null,
69
- category
70
- }, true),
71
- text: category.name
72
- });
73
- category = category.parent;
74
- }
75
- breadcrumbs.push(...categoriesBreadcrumbs);
76
- }
77
- if (this.currentProductType) {
78
- breadcrumbs.push({
77
+ const breadcrumbs = [
78
+ { to: '/products', text: 'Products' },
79
+ ...(this.currentProductType ? [{
79
80
  to: generateProductsLink(this.$route, {
80
81
  type: this.currentProductType.alias,
81
82
  brand: null,
@@ -83,31 +84,43 @@ export default {
83
84
  category: null
84
85
  }),
85
86
  text: this.currentProductType.name
86
- });
87
- }
88
-
89
- if (this.currentTag) {
90
- breadcrumbs.push({
87
+ }] : []),
88
+ ...(this.currentTag ? [{
91
89
  to: generateProductsLink(this.$route, {
92
90
  type: this.currentProductType?.alias,
93
- tag: this.currentTag?.alias,
91
+ tag: this.currentTag.alias,
94
92
  brand: null,
95
93
  category: null
96
94
  }),
97
95
  text: this.currentTag.name
98
- });
99
- }
100
-
101
- if (this.currentBrand) {
102
- breadcrumbs.push({
96
+ }] : []),
97
+ ...(this.currentBrand ? [{
103
98
  to: generateProductsLink(this.$route, {
104
99
  type: this.currentProductType?.alias,
105
100
  tag: this.currentTag?.alias,
106
- brand: this.currentBrand?.alias,
101
+ brand: this.currentBrand.alias,
107
102
  category: this.currentCategory?.alias
108
103
  }),
109
104
  text: this.currentBrand.name
110
- });
105
+ }] : [])
106
+ ];
107
+
108
+ if (this.currentCategory) {
109
+ const categoriesBreadcrumbs = [];
110
+ let category = this.currentCategory;
111
+ while (category) {
112
+ categoriesBreadcrumbs.unshift({
113
+ to: generateProductsLink(this.$route, {
114
+ type: null,
115
+ brand: null,
116
+ tag: null,
117
+ category
118
+ }, true),
119
+ text: category.name
120
+ });
121
+ category = category.parent;
122
+ }
123
+ breadcrumbs.splice(1, this.currentProductType ? 1 : 0, ...categoriesBreadcrumbs);
111
124
  }
112
125
 
113
126
  return breadcrumbs;
@@ -138,7 +138,9 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
138
138
  }
139
139
  },
140
140
  created() {
141
- this.fillBreadcrumbs();
141
+ if (this.product) {
142
+ this.fillBreadcrumbs();
143
+ }
142
144
  },
143
145
  async mounted() {
144
146
  if (!this.product) {
@@ -272,6 +274,8 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
272
274
  return '/shop/:shop/:sku/:product/editor';
273
275
  },
274
276
  jsonld() {
277
+ this.fillBreadcrumbs();
278
+
275
279
  const { name, aggregateRating, reviews, brand, SKU } = this.product;
276
280
  let brandAdditionalProductsRichSnippet = {};
277
281
  try {
package/nuxt.config.js CHANGED
@@ -1,7 +1,30 @@
1
1
  const sharedRoutes = require('./routes');
2
2
  const feeds = require('./feeds');
3
3
 
4
- module.exports = (config, axios, { raygunClient, publicPath, productUrlToEditor } = {}) => ({
4
+ function formatSitemapDate(date) {
5
+ const year = date.getFullYear();
6
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
7
+ const day = `${date.getDate()}`.padStart(2, '0');
8
+ return `${year}-${month}-${day}`;
9
+ }
10
+
11
+ function getCurrentWeekMonday() {
12
+ const date = new Date();
13
+ const day = date.getDay();
14
+ const diff = day === 0 ? -6 : 1 - day;
15
+ date.setDate(date.getDate() + diff);
16
+ date.setHours(0, 0, 0, 0);
17
+ return date;
18
+ }
19
+
20
+ function getCurrentMonthFirstDay() {
21
+ const date = new Date();
22
+ date.setDate(1);
23
+ date.setHours(0, 0, 0, 0);
24
+ return date;
25
+ }
26
+
27
+ module.exports = (config, axios, { raygunClient, publicPath, productUrlToEditor, sitemapWithoutProductTypes } = {}) => ({
5
28
  globalName: 'appLancom',
6
29
  mode: 'universal',
7
30
  head: {
@@ -27,23 +50,32 @@ module.exports = (config, axios, { raygunClient, publicPath, productUrlToEditor
27
50
  sitemap: {
28
51
  hostname: `https://${config.HOST_NAME}`,
29
52
  defaults: {
30
- priority: 1
53
+ lastmod: formatSitemapDate(getCurrentMonthFirstDay())
31
54
  },
32
55
  exclude: [
33
56
  // '/',
34
57
  '/faq',
35
58
  '/quotes/request',
36
- '/products/',
59
+ sitemapWithoutProductTypes ? '/products' : null,
60
+ sitemapWithoutProductTypes ? '/brands' : null,
37
61
  '/preview/**',
38
62
  '/checkout/**',
39
63
  '/customer/**'
40
- ],
64
+ ].filter(s => !!s),
41
65
  cacheTime: 10,
42
66
  routes: async () => {
43
- const { data } = await axios.get(`${config.LOCAL_API_URL}/feed/sitemap?host=${config.HOST_NAME}&toEditor=${!!productUrlToEditor}`);
67
+ const { data } = await axios.get(`${config.LOCAL_API_URL}/feed/sitemap?host=${config.HOST_NAME}&toEditor=${!!productUrlToEditor}&sitemapWithoutProductTypes=${!!sitemapWithoutProductTypes}`);
68
+ const defaultLastmod = formatSitemapDate(getCurrentWeekMonday());
44
69
  return [
45
- ...data.map(url => ({ url, priority: 0.8 })),
46
- // { url: `https://${config.HOST_NAME}`, priority: 1 }
70
+ { url: '/', lastmod: defaultLastmod },
71
+ ...data.map(item => {
72
+ const url = item.url || item.link || item;
73
+ const updatedAt = item.updatedAt;
74
+ return {
75
+ url,
76
+ lastmod: updatedAt ? formatSitemapDate(new Date(updatedAt)) : defaultLastmod
77
+ };
78
+ })
47
79
  ];
48
80
  }
49
81
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lancom/shared",
3
- "version": "0.0.469",
3
+ "version": "0.0.471",
4
4
  "description": "lancom common scripts",
5
5
  "author": "e.tokovenko <e.tokovenko@gmail.com>",
6
6
  "repository": {
@@ -0,0 +1,37 @@
1
+ module.exports = function (req, res, next) {
2
+ if ((req.url || '').split('?')[0] !== '/sitemap.xml') {
3
+ return next();
4
+ }
5
+
6
+ console.log('[sitemap] intercepting /sitemap.xml');
7
+
8
+ const host = req.headers.host;
9
+ const protocol = req.headers['x-forwarded-proto'] || 'https';
10
+ const rootUrl = `${protocol}://${host}`;
11
+
12
+ function fixXml(chunk) {
13
+ const isBuffer = Buffer.isBuffer(chunk);
14
+ const str = chunk.toString();
15
+ const fixed = str.split(`<loc>${rootUrl}/</loc>`).join(`<loc>${rootUrl}</loc>`);
16
+ console.log('[sitemap] write chunk, root replaced:', str !== fixed, '| rootUrl:', rootUrl);
17
+ return isBuffer ? Buffer.from(fixed) : fixed;
18
+ }
19
+
20
+ const write = res.write;
21
+ res.write = function (chunk, ...args) {
22
+ if (chunk) {
23
+ return write.call(this, fixXml(chunk), ...args);
24
+ }
25
+ return write.call(this, chunk, ...args);
26
+ };
27
+
28
+ const end = res.end;
29
+ res.end = function (chunk, ...args) {
30
+ if (chunk) {
31
+ return end.call(this, fixXml(chunk), ...args);
32
+ }
33
+ return end.call(this, chunk, ...args);
34
+ };
35
+
36
+ return next();
37
+ };
package/store/products.js CHANGED
@@ -59,6 +59,7 @@ export const actions = {
59
59
  products, count, page, perPage, productTypes, colors, colorGroups, brands, tags, attributes,
60
60
  categories, minPrice, maxPrice, category
61
61
  } = await api.fetchProducts(shop, params);
62
+ commit('setCategory', category);
62
63
  commit('setProducts', products);
63
64
  commit('setPage', page);
64
65
  commit('setCount', count);
@@ -68,7 +69,6 @@ export const actions = {
68
69
  commit('setColorGroups', colorGroups);
69
70
  commit('setBrands', brands);
70
71
  commit('setCategories', categories);
71
- commit('setCategory', category);
72
72
  commit('setTags', tags);
73
73
  commit('setAttributes', attributes);
74
74
  commit('setMinPrice', minPrice);
@@ -88,9 +88,9 @@ export const actions = {
88
88
  throw e;
89
89
  }
90
90
  },
91
- async fetchChildrenCategories({ commit }, category) {
91
+ async fetchChildrenCategories({ commit }, { category, shop }) {
92
92
  try {
93
- const categories = await api.fetchChildrenCategories(category?._id);
93
+ const categories = await api.fetchChildrenCategories(category?._id, shop._id);
94
94
  commit('setChildrenCategories', categories);
95
95
  return categories;
96
96
  } catch (e) {