@lancom/shared 0.0.453 → 0.0.454

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.
@@ -3,6 +3,7 @@ import { _get, _post, _put, _delete, _patch } from './helpers';
3
3
  import adminApi from './admin';
4
4
  import { unminifySimpleProducts } from './utils/simple-products';
5
5
  import { unminifyProduct } from './utils/product';
6
+ import { createCachedFunction } from './../utils/cache';
6
7
 
7
8
  const api = {
8
9
  fetchClientSettings(shop, params) {
@@ -93,12 +94,12 @@ const api = {
93
94
  const url = shop ? `shop/${shop}/products/${alias}/simple-products` : `products/${alias}/simple-products`;
94
95
  return unminifySimpleProducts(await _get(url, params));
95
96
  },
96
- fetchRelatedProducts(shop, alias, params) {
97
+ fetchRelatedProducts: createCachedFunction((shop, alias, params) => {
97
98
  return _get(`shop/${shop}/products/${alias}/related-products`, params);
98
- },
99
- fetchOtherProducts(shop, alias, params) {
99
+ }, 5000),
100
+ fetchOtherProducts: createCachedFunction((shop, alias, params) => {
100
101
  return _get(`shop/${shop}/products/${alias}/other-products`, params);
101
- },
102
+ }, 5000),
102
103
  fetchHelpMessages(shop, group) {
103
104
  return _get(`shop/${shop}/help-messages/${group}`);
104
105
  },
@@ -0,0 +1,33 @@
1
+ const createCachedFunction = (fn, ttl = 30000) => {
2
+ const cache = new Map();
3
+ const inFlight = new Map();
4
+
5
+ return (...args) => {
6
+ const cacheKey = JSON.stringify(args);
7
+ const cached = cache.get(cacheKey);
8
+ const now = Date.now();
9
+
10
+ if (cached && now - cached.timestamp < ttl) {
11
+ return Promise.resolve(cached.data);
12
+ }
13
+
14
+ const existing = inFlight.get(cacheKey);
15
+ if (existing) {
16
+ return existing;
17
+ }
18
+
19
+ const promise = fn(...args).then(data => {
20
+ cache.set(cacheKey, { data, timestamp: now });
21
+ inFlight.delete(cacheKey);
22
+ return data;
23
+ }).catch(error => {
24
+ inFlight.delete(cacheKey);
25
+ throw error;
26
+ });
27
+
28
+ inFlight.set(cacheKey, promise);
29
+ return promise;
30
+ };
31
+ };
32
+
33
+ export { createCachedFunction };
@@ -137,17 +137,19 @@ export default class FabricHelper {
137
137
  }
138
138
 
139
139
  setPrintArea(printArea, size, product) {
140
- this.printAreaRect = getPrintAreaByName({
141
- printArea: printArea?.parentPrintArea || printArea?._id,
142
- printSize: printArea?.printSize,
143
- printAreaOffsets: printArea?.printAreaOffsets,
144
- editorWidth: size.width,
145
- editorHeight: size.height
146
- }, product, true);
147
- if (this.background) {
148
- this.background.setBoundingRect(this.printAreaRect);
140
+ if (printArea) {
141
+ this.printAreaRect = getPrintAreaByName({
142
+ printArea: printArea?.parentPrintArea || printArea?._id,
143
+ printSize: printArea?.printSize,
144
+ printAreaOffsets: printArea?.printAreaOffsets,
145
+ editorWidth: size.width,
146
+ editorHeight: size.height
147
+ }, product, true);
148
+ if (this.background) {
149
+ this.background.setBoundingRect(this.printAreaRect);
150
+ }
151
+ this.addBoundingArea();
149
152
  }
150
- this.addBoundingArea();
151
153
  }
152
154
 
153
155
  addBoundingArea() {
@@ -179,6 +179,7 @@ export default {
179
179
  &s--primary &.disabled,
180
180
  &s--secondary &.disabled {
181
181
  pointer-events: none;
182
+ opacity: 0.7;
182
183
  }
183
184
  &s--large &.short,
184
185
  &s--primary &.short,
@@ -43,7 +43,7 @@
43
43
  </div>
44
44
  </div>
45
45
  <div
46
- v-if="fabricHelper && !editModeSelectedLayer"
46
+ v-if="fabricHelper && fabricHelper.printAreaRect && !editModeSelectedLayer"
47
47
  class="EditorWorkspaceSide__placeholder"
48
48
  :class="{
49
49
  tighten: !isZoomed && printAreaIsSmall,
@@ -233,7 +233,7 @@ export default {
233
233
  return layers;
234
234
  },
235
235
  positionPlaceholder() {
236
- const { center, left, top, width, height } = this.fabricHelper.printAreaRect;
236
+ const { center, left, top, width, height } = this.fabricHelper.printAreaRect || {};
237
237
  const ratio = this.calcWorkspaceSize() / this.editorSize.width;
238
238
  if (this.printAreaLayers.length > 0) {
239
239
  return {
@@ -0,0 +1,35 @@
1
+ .ClearanceProducts {
2
+ &__head {
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: space-between;
6
+ margin: 4px 0 16px;
7
+ gap: 16px;
8
+ }
9
+
10
+ &__title {
11
+ color: #0A0A0A;
12
+ font-size: 14px;
13
+ font-style: normal;
14
+ font-weight: 600;
15
+ line-height: 20px;
16
+ }
17
+
18
+ &__all {
19
+ color: #194BB3;
20
+ text-align: center;
21
+ font-size: 12px;
22
+ font-style: normal;
23
+ font-weight: 600;
24
+ line-height: 16px;
25
+ text-decoration: underline;
26
+ &:hover {
27
+ text-decoration: none;
28
+ }
29
+ }
30
+ &__list {
31
+ display: grid;
32
+ gap: 14px;
33
+ }
34
+ }
35
+
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <section
3
+ class="ClearanceProducts__wrapper">
4
+ <div class="ClearanceProducts__head">
5
+ <h2 class="ClearanceProducts__title">
6
+ Clearance Product
7
+ </h2>
8
+ <a
9
+ href="/c/clearance"
10
+ class="ClearanceProducts__all">
11
+ View all clearance products
12
+ </a>
13
+ </div>
14
+ <div class="ClearanceProducts__list">
15
+ <clearance-product
16
+ v-for="item in products"
17
+ :key="item._id"
18
+ :product="item"
19
+ :to-editor="toEditor"
20
+ class="ClearanceProducts__product" />
21
+ </div>
22
+ </section>
23
+ </template>
24
+
25
+ <script>
26
+ import { mapGetters } from 'vuex';
27
+ import api from '@lancom/shared/assets/js/api';
28
+ import ClearanceProduct from './clearance_product/clearance-product';
29
+
30
+ export default {
31
+ name: 'ClearanceProducts',
32
+ components: {
33
+ ClearanceProduct
34
+ },
35
+ props: {
36
+ product: {
37
+ type: Object,
38
+ required: true
39
+ },
40
+ toEditor: {
41
+ type: Boolean,
42
+ default: false
43
+ }
44
+ },
45
+ data() {
46
+ return {
47
+ products: [],
48
+ loading: false
49
+ };
50
+ },
51
+ computed: {
52
+ ...mapGetters(['shop', 'country', 'currency'])
53
+ },
54
+ mounted() {
55
+ this.loadProducts();
56
+ },
57
+ methods: {
58
+ isClearanceColor(color) {
59
+ return color?.pricing?.some(c => c.clearance);
60
+ },
61
+ hasClearanceColors(product) {
62
+ return (product.colors || []).some(color => this.isClearanceColor(color));
63
+ },
64
+ async loadProducts() {
65
+ try {
66
+ this.loading = true;
67
+ const { products } = await api.fetchRelatedProducts(this.shop._id, this.product.alias, {
68
+ needShuffle: false,
69
+ country: this.country?._id,
70
+ currency: this.currency?._id
71
+ });
72
+ const clearanceProducts = products.filter(p => p.isClearance || this.hasClearanceColors(p));
73
+ this.products = clearanceProducts.slice(0, 1);
74
+ } catch (e) {
75
+ } finally {
76
+ this.loading = false;
77
+ }
78
+ }
79
+ }
80
+ };
81
+ </script>
82
+
83
+ <style lang="scss" scoped>
84
+ @import 'clearance-products';
85
+ </style>
@@ -0,0 +1,82 @@
1
+ .ClearanceProduct {
2
+ &__wrapper {
3
+ display: flex;
4
+ gap: 12px;
5
+ padding: 12px;
6
+ border-radius: 8px;
7
+ border: 1px solid #E5E5E5;
8
+ background: #FFF;
9
+ }
10
+
11
+ &__img {
12
+ width: 64px;
13
+ height: 64px;
14
+ object-fit: contain;
15
+ border-radius: 4px;
16
+ }
17
+
18
+ &__body {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 4px;
22
+ flex: 1;
23
+ }
24
+
25
+ &__row {
26
+ display: flex;
27
+ gap: 8px;
28
+ }
29
+
30
+ &__title {
31
+ color: #0A0A0A;
32
+ font-size: 14px;
33
+ font-style: normal;
34
+ font-weight: 600;
35
+ line-height: 20px;
36
+ text-decoration: none;
37
+ &:hover {
38
+ text-decoration: underline;
39
+ }
40
+ }
41
+
42
+ &__sku {
43
+ color: #666;
44
+ font-size: 12px;
45
+ font-style: normal;
46
+ font-weight: 400;
47
+ line-height: 16px;
48
+ }
49
+
50
+ &__clearance-label {
51
+ color: #D97706;
52
+ font-size: 12px;
53
+ font-style: normal;
54
+ font-weight: 600;
55
+ line-height: 16px;
56
+ }
57
+
58
+ &__price {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 8px;
62
+ margin-top: auto;
63
+ }
64
+
65
+ &__price-current {
66
+ color: #0A0A0A;
67
+ font-size: 14px;
68
+ font-style: normal;
69
+ font-weight: 600;
70
+ line-height: 20px;
71
+ }
72
+
73
+ &__price-old {
74
+ color: #999;
75
+ font-size: 12px;
76
+ font-style: normal;
77
+ font-weight: 400;
78
+ line-height: 16px;
79
+ text-decoration: line-through;
80
+ }
81
+ }
82
+
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <div class="ClearanceProduct__wrapper">
3
+ <a
4
+ v-if="productСover"
5
+ :href="productLink">
6
+ <img
7
+ :src="productСover"
8
+ :alt="product.name"
9
+ class="ClearanceProduct__img" />
10
+ </a>
11
+ <div class="ClearanceProduct__body">
12
+ <div class="ClearanceProduct__row">
13
+ <a
14
+ :href="productLink"
15
+ class="ClearanceProduct__title">
16
+ {{ product.name }}
17
+ </a>
18
+ </div>
19
+ <div
20
+ v-if="product.SKU"
21
+ class="ClearanceProduct__sku">
22
+ sku: {{ product.SKU }}
23
+ </div>
24
+ <div class="ClearanceProduct__price">
25
+ <span class="ClearanceProduct__price-current">
26
+ <price
27
+ :price="currentColorMaxPrice"
28
+ :with-gst="priceIncludeGST" />
29
+ </span>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script>
36
+ import productPreview from '@lancom/shared/mixins/product-preview';
37
+ import Price from '@lancom/shared/components/common/price';
38
+
39
+ export default {
40
+ name: 'ClearanceProduct',
41
+ components: {
42
+ Price
43
+ },
44
+ mixins: [productPreview],
45
+ props: {
46
+ toEditor: {
47
+ type: Boolean,
48
+ default: false
49
+ }
50
+ }
51
+ };
52
+ </script>
53
+
54
+ <style lang="scss" scoped>
55
+ @import 'clearance-product';
56
+ </style>
@@ -4,15 +4,19 @@
4
4
  <h2 class="OtherProducts__title">
5
5
  Other Products
6
6
  </h2>
7
- <a href="#" class="OtherProducts__all">
7
+ <a
8
+ v-if="categoryLink"
9
+ :href="categoryLink"
10
+ class="OtherProducts__all">
8
11
  View all products in this category
9
12
  </a>
10
13
  </div>
11
14
  <div class="OtherProducts__list">
12
15
  <other-product
13
- v-for="product in products"
14
- :key="product._id"
15
- :product="product"
16
+ v-for="item in products"
17
+ :key="item._id"
18
+ :product="item"
19
+ :to-editor="toEditor"
16
20
  class="OtherProducts__product" />
17
21
  </div>
18
22
  </section>
@@ -35,7 +39,7 @@ export default {
35
39
  },
36
40
  toEditor: {
37
41
  type: Boolean,
38
- default: true
42
+ default: false
39
43
  },
40
44
  limit: {
41
45
  type: Number,
@@ -49,21 +53,44 @@ export default {
49
53
  };
50
54
  },
51
55
  computed: {
52
- ...mapGetters(['shop', 'country', 'currency'])
56
+ ...mapGetters(['shop', 'country', 'currency']),
57
+ categoryLink() {
58
+ const category = this.product.category;
59
+ if (!category) {
60
+ return null;
61
+ }
62
+ const categories = [];
63
+ let current = category;
64
+ while (current) {
65
+ categories.unshift(current.alias);
66
+ current = current.parent;
67
+ }
68
+ return categories.length ? `/c/${categories.join('/')}` : null;
69
+ }
53
70
  },
54
71
  mounted() {
55
72
  this.loadProducts();
56
73
  },
57
74
  methods: {
75
+ isClearanceColor(color) {
76
+ return color?.pricing?.some(c => c.clearance);
77
+ },
78
+ hasClearanceColors(product) {
79
+ return (product.colors || []).some(color => this.isClearanceColor(color));
80
+ },
58
81
  async loadProducts() {
59
82
  try {
60
83
  this.loading = true;
61
- const condition = {
84
+ const { products } = await api.fetchRelatedProducts(this.shop._id, this.product.alias, {
85
+ needShuffle: false,
62
86
  country: this.country?._id,
63
- currency: this.currency?._id,
64
- limit: this.limit
65
- };
66
- this.products = (await api.fetchOtherProducts(this.shop._id, this.product.alias, condition)).products;
87
+ currency: this.currency?._id
88
+ });
89
+ this.products = products.sort((a, b) => {
90
+ const aIsClearance = a.isClearance || this.hasClearanceColors(a);
91
+ const bIsClearance = b.isClearance || this.hasClearanceColors(b);
92
+ return aIsClearance === bIsClearance ? 0 : aIsClearance ? -1 : 1;
93
+ });
67
94
  } catch (e) {
68
95
  } finally {
69
96
  this.loading = false;
@@ -21,28 +21,10 @@
21
21
  class="OtherProduct__sku">
22
22
  sku: {{ product.SKU }}
23
23
  </div>
24
- <div
25
- v-if="product.isClearance"
26
- class="OtherProduct__price">
27
- <span
28
- v-if="product.oldPrice"
29
- class="OtherProduct__price-old">
30
- <price
31
- :price="minPrice"
32
- :with-gst="priceIncludeGST" />
33
- </span>
34
- <span class="OtherProduct__price-current">
35
- <price
36
- :price="product.minPrice"
37
- :with-gst="priceIncludeGST" />
38
- </span>
39
- </div>
40
- <div
41
- v-else
42
- class="OtherProduct__price">
24
+ <div class="OtherProduct__price">
43
25
  <span class="OtherProduct__price-current">
44
26
  <price
45
- :price="minPrice"
27
+ :price="currentColorMaxPrice"
46
28
  :with-gst="priceIncludeGST" />
47
29
  </span>
48
30
  </div>
@@ -59,7 +41,13 @@ export default {
59
41
  components: {
60
42
  Price
61
43
  },
62
- mixins: [productPreview]
44
+ mixins: [productPreview],
45
+ props: {
46
+ toEditor: {
47
+ type: Boolean,
48
+ default: false
49
+ }
50
+ }
63
51
  };
64
52
  </script>
65
53
 
@@ -37,11 +37,15 @@ export default {
37
37
  };
38
38
  },
39
39
  async fetch() {
40
- const { products } = await api.fetchRelatedProducts(this.shop._id, this.product.alias, { needShuffle: true });
40
+ const { products } = await api.fetchRelatedProducts(this.shop._id, this.product.alias, {
41
+ needShuffle: false,
42
+ country: this.country?._id,
43
+ currency: this.currency?._id
44
+ });
41
45
  this.products = products.splice(0, 6);
42
46
  },
43
47
  computed: {
44
- ...mapGetters(['shop']),
48
+ ...mapGetters(['shop', 'country', 'currency']),
45
49
  hasProducts() {
46
50
  return this.products.length > 0;
47
51
  }
@@ -2,6 +2,7 @@ import { mapGetters } from 'vuex';
2
2
  import { getColorBackgroundStyle, getProductMediumCover, getBgStyle, getProductHoverCover } from '@lancom/shared/assets/js/utils/colors';
3
3
  import { staticLink } from '@lancom/shared/assets/js/utils/filters';
4
4
  import { generateProductLink } from '@lancom/shared/assets/js/utils/product';
5
+ import { sortSizes } from '@lancom/shared/assets/js/utils/sizes';
5
6
 
6
7
  const loadHolder = {
7
8
  canLoadImages: true,
@@ -129,6 +130,68 @@ const productPreview = {
129
130
  case 'child':
130
131
  return 'icon-baby';
131
132
  }
133
+ },
134
+
135
+ colors() {
136
+ const colors = this.product.colors?.filter(c => !!c) || [];
137
+ const sortedColors = colors.sort((a, b) => {
138
+ const aIsClearance = this.isClearanceColor(a);
139
+ const bIsClearance = this.isClearanceColor(b);
140
+ return aIsClearance === bIsClearance ? 0 : aIsClearance ? -1 : 1;
141
+ });
142
+ return this.full ? sortedColors : sortedColors.slice(0, this.maxVisibleColors);
143
+ },
144
+ hasSizes() {
145
+ return this.sizes.length > 0;
146
+ },
147
+ sizes() {
148
+ const sizes = sortSizes(this.product.sizes || []);
149
+ return this.full ? sizes : sizes.slice(0, this.maxVisibleSizes);
150
+ },
151
+ hiddenSizesCount() {
152
+ const sizes = this.product.sizes || [];
153
+ return sizes.length - this.sizes.length;
154
+ },
155
+ isVisibleShowMore() {
156
+ const colors = this.product.colors || [];
157
+ return (colors.length > this.maxVisibleColors) && !this.full;
158
+ },
159
+ mainColor() {
160
+ return this.currentColor || this.colorWithMaxPrice;
161
+ },
162
+ colorWithMaxPrice() {
163
+ const colorWithMaxPrice = this.product.colors?.reduce((max, current) => {
164
+ const currentMaxPrice = current?.maxPrice || 0;
165
+ const maxPrice = max?.maxPrice || 0;
166
+ return (!max || (current && currentMaxPrice > maxPrice)) ? current : max;
167
+ }, null);
168
+ return colorWithMaxPrice;
169
+ },
170
+ visibleTiers() {
171
+ const pricing = (this.full ? this.mainColor?.pricing : this.mainColor?.pricing?.slice(0, this.maxVisibleTiers)) || [];
172
+ const printsPrice = this.product.minPrintsPrice || 0;
173
+ return pricing.map(p => ({ ...p, price: p.price + printsPrice }));
174
+ },
175
+ currentColorMaxPrice() {
176
+ const printsPrice = this.product.maxPrintsPrice || 0;
177
+ const productPrice = this.mainColor?.maxPrice || 0;
178
+ return productPrice + printsPrice;
179
+ },
180
+ currentColorMaxPriceWithoutClearance() {
181
+ const printsPrice = this.product.maxPrintsPrice || 0;
182
+ const productPrice = this.mainColor?.maxPriceWithoutClearance || 0;
183
+ return productPrice + printsPrice;
184
+ },
185
+ minClearanceColorPrice() {
186
+ const clearanceColors = this.product.colors?.filter(c => this.isClearanceColor(c)) || [];
187
+ const minClearanceColor = clearanceColors.reduce((min, current) => {
188
+ const currentMinPrice = current?.minPrice || 0;
189
+ const minPrice = min?.minPrice || 0;
190
+ return (!min || (current && currentMinPrice < minPrice)) ? current : min;
191
+ }, null);
192
+ const printsPrice = this.product.minPrintsPrice || 0;
193
+ const productPrice = minClearanceColor?.minPrice || 0;
194
+ return minClearanceColor ? productPrice + printsPrice : 0;
132
195
  }
133
196
  },
134
197
  mounted() {
@@ -125,8 +125,8 @@ export default (IS_PRODUCT_PRESET_PRINT_PRICING, isEditor = false) => ({
125
125
  return (thumbProductImages.length > 0 ? thumbProductImages : this.images).slice(0, 6);
126
126
  },
127
127
  mainProductImageSrc() {
128
- const image = (this.isEditor && this.mainProductImage.extralarge) || this.mainProductImage.large;
129
- return this.mainProductImage && staticLink(image);
128
+ const image = (this.isEditor && this.mainProductImage?.extralarge) || this.mainProductImage?.large;
129
+ return image && staticLink(image);
130
130
  },
131
131
  mainProductImageStyles() {
132
132
  return this.mainProductImageSrc ? { 'background-image': `url(${this.mainProductImageSrc});` } : {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lancom/shared",
3
- "version": "0.0.453",
3
+ "version": "0.0.454",
4
4
  "description": "lancom common scripts",
5
5
  "author": "e.tokovenko <e.tokovenko@gmail.com>",
6
6
  "repository": {