@lancom/shared 0.0.206 → 0.0.208

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.
@@ -30,6 +30,53 @@
30
30
  margin-right: 22px;
31
31
  }
32
32
  }
33
+ &__upload {
34
+ display: flex;
35
+ &--uploading {
36
+ pointer-events: none;
37
+ opacity: 0.7;
38
+ }
39
+ &-btn {
40
+ font-weight: bold;
41
+ font-size: 16px;
42
+ line-height: 22px;
43
+ text-transform: uppercase;
44
+ background-color: $green;
45
+ padding: 8px 14px;
46
+ &--disabled {
47
+ background-color: $gray;
48
+ }
49
+ &--uploaded {
50
+ opacity: 0.5;
51
+ }
52
+ }
53
+ &-info {
54
+ font-size: 12px;
55
+ line-height: 16px;
56
+ color: $black;
57
+ margin-left: 12px;
58
+ }
59
+ &-progress {
60
+ position: absolute;
61
+ left: 0;
62
+ top: 0;
63
+ right: 0;
64
+ bottom: 0;
65
+ }
66
+ &-file {
67
+ margin-top: 5px;
68
+ font-size: 12px;
69
+ a {
70
+ text-decoration: none;
71
+ color: $black;
72
+ }
73
+ }
74
+ }
75
+ &__error {
76
+ margin-top: 5px;
77
+ font-size: 12px;
78
+ color: $error;
79
+ }
33
80
  }
34
81
  ::v-deep .AddReview__content {
35
82
  input,
@@ -9,7 +9,7 @@
9
9
  tag="form"
10
10
  class="AddReview__form">
11
11
  <div class="row">
12
- <div class="col-sm-6 col-12">
12
+ <div class="col-sm-7 col-12">
13
13
  <validation-provider
14
14
  v-slot="{ errors }"
15
15
  tag="div"
@@ -40,7 +40,7 @@
40
40
  </span>
41
41
  </validation-provider>
42
42
  </div>
43
- <div class="col-sm-6 col-12">
43
+ <div class="col-sm-5 col-12">
44
44
  <validation-provider
45
45
  v-slot="{ errors }"
46
46
  tag="div"
@@ -72,8 +72,10 @@
72
72
  </validation-provider>
73
73
  </div>
74
74
  </div>
75
- <div class="row">
76
- <div class="col-12">
75
+ <div
76
+ class="row"
77
+ style="margin-top: -20px;">
78
+ <div class="col-sm-7 col-12">
77
79
  <validation-provider
78
80
  v-slot="{ errors }"
79
81
  tag="div"
@@ -103,6 +105,113 @@
103
105
  </span>
104
106
  </validation-provider>
105
107
  </div>
108
+ <div class="col-sm-5 col-12">
109
+ <div>
110
+ <validation-provider
111
+ v-slot="{ errors }"
112
+ tag="div"
113
+ name="Pro"
114
+ class="form-row">
115
+ <input
116
+ id="add-review-pro"
117
+ ref="pro"
118
+ v-model="form.pro"
119
+ name="pro"
120
+ type="text"
121
+ class="form-field"
122
+ :class="{
123
+ 'is-danger': errors.length,
124
+ filled: form.pro
125
+ }" />
126
+ <label
127
+ for="add-review-pro"
128
+ class="form-label label-inner">
129
+ Pro
130
+ </label>
131
+ <span
132
+ v-if="errors.length"
133
+ class="form-help is-danger">
134
+ {{ errors[0] }}
135
+ </span>
136
+ </validation-provider>
137
+ </div>
138
+ <div class="mt-10">
139
+ <validation-provider
140
+ v-slot="{ errors }"
141
+ tag="div"
142
+ name="Cons"
143
+ class="form-row">
144
+ <input
145
+ id="add-review-cons"
146
+ ref="cons"
147
+ v-model="form.cons"
148
+ name="cons"
149
+ type="text"
150
+ class="form-field"
151
+ :class="{
152
+ 'is-danger': errors.length,
153
+ filled: form.cons
154
+ }" />
155
+ <label
156
+ for="add-review-cons"
157
+ class="form-label label-inner">
158
+ Cons
159
+ </label>
160
+ <span
161
+ v-if="errors.length"
162
+ class="form-help is-danger">
163
+ {{ errors[0] }}
164
+ </span>
165
+ </validation-provider>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ <div class="form-row">
170
+ <file-uploader
171
+ url="reviews/image"
172
+ :multiple="false"
173
+ :has-conversion-error-modal="false"
174
+ :show-error-message="false"
175
+ @onchange="handleUploadChange"
176
+ @onerror="handleUploadError"
177
+ @onuploaded="handleUploaded">
178
+ <template v-slot:toggle="{ uploading }">
179
+ <div
180
+ class="AddReview__upload"
181
+ :class="{
182
+ 'AddReview__upload--uploading': uploading
183
+ }">
184
+ <div
185
+ class="AddReview__upload-btn"
186
+ :class="{
187
+ 'AddReview__upload-btn--disabled': uploading,
188
+ 'AddReview__upload-btn--uploaded': form.image
189
+ }">
190
+ Upload
191
+ </div>
192
+ <div class="AddReview__upload-info">
193
+ <div>Note: accepted file types:</div>
194
+ <div><b>JPEG / PNG / AI / EPS / PDF</b></div>
195
+ </div>
196
+ </div>
197
+ </template>
198
+ <template v-slot:progress="{ progress }">
199
+ <div
200
+ v-if="progress"
201
+ class="AddReview__upload-progress">
202
+ <spinner background="black" />
203
+ </div>
204
+ </template>
205
+ </file-uploader>
206
+ <div
207
+ v-if="uploadError"
208
+ class="AddReview__error">
209
+ {{ uploadError }}
210
+ </div>
211
+ <product-review-image
212
+ v-if="form.image"
213
+ style="margin-top: 10px;"
214
+ :review="form" />
106
215
  </div>
107
216
  <div class="AddReview__mark">
108
217
  <div class="AddReview__mark-label">
@@ -128,14 +237,18 @@
128
237
  </template>
129
238
 
130
239
  <script>
240
+ import FileUploader from '@lancom/shared/components/common/file_uploader';
131
241
  import StarsMark from '@lancom/shared/components/common/stars-mark';
132
242
  import api from '@lancom/shared/assets/js/api';
133
243
  import { mapGetters } from 'vuex';
244
+ import ProductReviewImage from './../product_review/product_review_image/product-review-image';
134
245
 
135
246
  export default {
136
247
  name: 'AddReview',
137
248
  components: {
138
- StarsMark
249
+ FileUploader,
250
+ StarsMark,
251
+ ProductReviewImage
139
252
  },
140
253
  props: {
141
254
  product: {
@@ -145,11 +258,16 @@ export default {
145
258
  },
146
259
  data() {
147
260
  return {
261
+ uploadError: null,
262
+ processing: false,
148
263
  form: {
149
264
  name: '',
150
265
  email: '',
151
266
  text: '',
152
- mark: 4
267
+ pro: '',
268
+ cons: '',
269
+ mark: 4,
270
+ image: null
153
271
  }
154
272
  };
155
273
  },
@@ -157,6 +275,17 @@ export default {
157
275
  ...mapGetters(['shop'])
158
276
  },
159
277
  methods: {
278
+ handleUploaded(file) {
279
+ this.form.image = file;
280
+ },
281
+ handleUploadError(e) {
282
+ const { error, message } = e?.response?.data || {};
283
+ this.uploadError = error || message || 'Failed upload image';
284
+ },
285
+ handleUploadChange() {
286
+ this.uploadError = null;
287
+ this.form.image = null;
288
+ },
160
289
  async submit() {
161
290
  try {
162
291
  this.processing = true;
@@ -39,6 +39,20 @@ export default {
39
39
  ProductReview
40
40
  },
41
41
  mixins: [],
42
+ data() {
43
+ const reviews = this.product.reviews || [];
44
+ const [, mainReviewId] = (this.$route.hash || '').match(/review-([a-z0-9]+)/i) || [];
45
+ const mainReview = reviews.find(review => review._id === mainReviewId);
46
+ console.log('mainReviewId: ', mainReviewId);
47
+ return {
48
+ scrollInterval: null,
49
+ mainReview,
50
+ reviews: [
51
+ mainReview,
52
+ ...reviews.filter(review => review !== mainReview)
53
+ ].filter(review => !!review)
54
+ };
55
+ },
42
56
  props: {
43
57
  product: {
44
58
  type: Object,
@@ -48,11 +62,31 @@ export default {
48
62
  computed: {
49
63
  hasReviews() {
50
64
  return this.reviews.length > 0;
51
- },
52
- reviews() {
53
- return this.product.reviews || [];
54
65
  }
55
66
  },
67
+ mounted() {
68
+ setTimeout(() => {
69
+ if (this.mainReview) {
70
+ const [first, second] = document.querySelectorAll(`#review-${this.mainReview._id}`) || [];
71
+ const mainReviewEl = second || first;
72
+ let top = 0;
73
+ if (mainReviewEl) {
74
+ this.scrollInterval = setInterval(() => {
75
+ top += 50;
76
+ window.scroll(0, top);
77
+ const rect = mainReviewEl.getBoundingClientRect();
78
+ console.log('mainReviewEl: ', mainReviewEl, rect.top);
79
+ if (rect.top < 300) {
80
+ clearInterval(this.scrollInterval);
81
+ }
82
+ }, 50);
83
+ }
84
+ }
85
+ }, 100);
86
+ },
87
+ destroyed() {
88
+ clearInterval(this.scrollInterval);
89
+ },
56
90
  methods: {
57
91
  showAddReviewModal() {
58
92
  const params = {
@@ -37,6 +37,14 @@
37
37
  margin-bottom: 10px;
38
38
  }
39
39
  }
40
+ &__info {
41
+ font-size: 11px;
42
+ margin-top: 6px;
43
+ color: grey;
44
+ }
45
+ &__info-wrapper {
46
+ display: flex;
47
+ }
40
48
  &__readmore {
41
49
  font-weight: 800;
42
50
  font-size: 16px;
@@ -1,11 +1,36 @@
1
1
  <template>
2
- <div class="ProductReview__wrapper">
2
+ <div
3
+ :id="`review-${review._id}`"
4
+ class="ProductReview__wrapper">
3
5
  <div class="ProductReview__name">
4
6
  {{ review.name }}
5
7
  </div>
6
8
  <div class="ProductReview__mark">
7
9
  <stars-mark v-model="review.mark" :disabled="true" />
8
10
  </div>
11
+ <div
12
+ v-if="review.pro || review.cons || review.image"
13
+ class="ProductReview__info-wrapper">
14
+ <div
15
+ v-if="review.image"
16
+ style="margin-right: 10px; margin-top: 7px;">
17
+ <product-review-image
18
+ :review="review"
19
+ :size="50" />
20
+ </div>
21
+ <div>
22
+ <div
23
+ v-if="review.pro"
24
+ class="ProductReview__info">
25
+ Pro: {{ review.pro }}
26
+ </div>
27
+ <div
28
+ v-if="review.cons"
29
+ class="ProductReview__info">
30
+ Cons: {{ review.cons }}
31
+ </div>
32
+ </div>
33
+ </div>
9
34
  <div
10
35
  ref="text"
11
36
  class="ProductReview__text"
@@ -24,11 +49,13 @@
24
49
  <script>
25
50
  import StarsMark from '@lancom/shared/components/common/stars-mark';
26
51
  import { nl2p } from '@lancom/shared/assets/js/utils/filters';
52
+ import ProductReviewImage from './product_review_image/product-review-image';
27
53
 
28
54
  export default {
29
55
  name: 'ProductReviews',
30
56
  components: {
31
- StarsMark
57
+ StarsMark,
58
+ ProductReviewImage
32
59
  },
33
60
  props: {
34
61
  review: {
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <div class="ProductReviewImage__wrapper">
3
+ <img
4
+ :src="review.image.small"
5
+ :style="{
6
+ width: `${size}px`
7
+ }"
8
+ @click="showImage">
9
+ </div>
10
+ </template>
11
+
12
+ <script>
13
+ import ImageViewer from '@lancom/shared/components/common/image_viewer/image-viewer';
14
+
15
+ export default {
16
+ name: 'ProductReviewImage',
17
+ props: {
18
+ review: {
19
+ type: Object,
20
+ required: true
21
+ },
22
+ size: {
23
+ type: Number,
24
+ default: 100
25
+ }
26
+ },
27
+ methods: {
28
+ showImage() {
29
+ this.$modal.show(
30
+ ImageViewer,
31
+ {
32
+ items: [{
33
+ src: this.review.image.large
34
+ }],
35
+ index: 0
36
+ },
37
+ {
38
+ name: 'image-viewer-modal',
39
+ root: this.$root,
40
+ width: '100%',
41
+ height: 'auto',
42
+ adaptive: true,
43
+ clickToClose: true,
44
+ transition: 'from-top-to-bottom'
45
+ }
46
+ );
47
+ }
48
+ }
49
+ };
50
+ </script>
51
+
52
+ <style lang="scss" scoped>
53
+ @import 'product-review-image.scss';
54
+ </style>
@@ -30,12 +30,12 @@
30
30
  <tr>
31
31
  <td>
32
32
  <div><b>QUOTE</b></div>
33
- <div>{{ quote.fullName }}</div>
34
- <div v-if="quote.company">{{ quote.company }}</div>
35
- <div v-if="quote.phone">{{ quote.phone }}</div>
36
- <div>{{ quote.email }}</div>
33
+ <div>{{ quote.address.fullName }}</div>
34
+ <div v-if="quote.address.company">{{ quote.address.company }}</div>
35
+ <div v-if="quote.address.phone">{{ quote.address.phone }}</div>
36
+ <div>{{ quote.address.email }}</div>
37
37
  <div>{{ quoteAddress }}</div>
38
- <div>{{ quote.additionalInfo }}</div>
38
+ <div>{{ quote.address.additionalInfo }}</div>
39
39
  </td>
40
40
  <td class="w-50">
41
41
  <div><b>DIRECT DEPOSIT DETAILS</b></div>
package/nuxt.config.js CHANGED
@@ -98,7 +98,8 @@ module.exports = (config, axios, { raygunClient, publicPath } = {}) => ({
98
98
  if (isClient) {
99
99
  config.devtool = 'source-map';
100
100
  }
101
- }
101
+ },
102
+ extractCSS: !config.IS_LOCAL,
102
103
  },
103
104
  hooks: {
104
105
  render: {
@@ -110,12 +111,112 @@ module.exports = (config, axios, { raygunClient, publicPath } = {}) => ({
110
111
  },
111
112
  },
112
113
  feed: [{
114
+ path: '/pr-rev-au.xml',
115
+ async get() {
116
+ const { data } = await axios.get(`${config.LOCAL_API_URL}/feed/reviews?host=${config.HOST_NAME}`);
117
+
118
+ return {
119
+ _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } },
120
+ feed: {
121
+ _attributes: {
122
+ 'xmlns:vc': 'http://www.w3.org/2007/XMLSchema-versioning',
123
+ 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
124
+ 'xsi:noNamespaceSchemaLocation': 'http://www.google.com/shopping/reviews/schema/product/2.3/product_reviews.xsd'
125
+ },
126
+ version: { _text: '2.3' },
127
+ publisher: {
128
+ name: { _text: 'Workdepot Australia' },
129
+ favicon: { _text: 'https://www.workdepot.com.au/favicon.png' }
130
+ },
131
+ reviews: {
132
+ review: [
133
+ ...data
134
+ .filter(review => !!review.product)
135
+ .map(review => {
136
+ const { product } = review;
137
+ const productUrl = `https://${config.HOST_NAME}/${product.brand.alias}/${product.productType.alias}/${product.alias}`;
138
+ const item = {
139
+ review_id: { _text: review._id },
140
+ reviewer: {
141
+ name: {
142
+ _attributes: {
143
+ is_anonymous: 'false'
144
+ },
145
+ _text: review.name
146
+ }
147
+ },
148
+ review_timestamp: { _text: review.createdAt },
149
+ content: { _text: review.text },
150
+ review_url: {
151
+ _attributes: {
152
+ type: 'singleton'
153
+ },
154
+ _text: `${productUrl}#review-${review._id}`
155
+ },
156
+ ratings: {
157
+ overall: {
158
+ _attributes: {
159
+ min: 1,
160
+ max: 5
161
+ },
162
+ _text: review.mark
163
+ }
164
+ },
165
+ products: {
166
+ product: {
167
+ product_ids: {
168
+ // gtins: {
169
+ // gtin: { _text: '541710238425' }
170
+ // },
171
+ // mpns: {
172
+ // mpn: { _text: '60101-10000' }
173
+ // },
174
+ skus: {
175
+ sku: { _text: review.product.SKU }
176
+ },
177
+ brands: {
178
+ brand: { _text: review.product.brand.name }
179
+ },
180
+ // asins: {
181
+ // asin: { _text: 'B07YMJ57MB' }
182
+ // }
183
+ },
184
+ product_name: { _text: review.product.name },
185
+ product_url: { _text: productUrl }
186
+ }
187
+ }
188
+ };
189
+ if (review.pro) {
190
+ item.pros = {
191
+ pro: [{ _text: review.pro }]
192
+ };
193
+ }
194
+ if (review.cons) {
195
+ item.cons = {
196
+ con: [{ _text: review.cons }]
197
+ };
198
+ }
199
+ if (review.image) {
200
+ item.reviewer_images = {
201
+ reviewer_image: [{
202
+ url: { _text: review.image.large }
203
+ }]
204
+ };
205
+ }
206
+ return item;
207
+ })
208
+ ]
209
+ }
210
+ }
211
+ };
212
+ }
213
+ }, {
113
214
  path: '/google-shopping.xml',
114
215
  async get() {
115
216
  const { data } = await axios.get(`${config.LOCAL_API_URL}/feed/products?host=${config.HOST_NAME}`);
116
217
  const spliceFirstImage = images => (images || []).splice(0, 1)[0];
117
218
  const getImages = images => (images || []).length > 0 ? images : null;
118
- return {
219
+ const channel = {
119
220
  title: { _text: 'All products' },
120
221
  link: { _text: `https://${config.HOST_NAME}` },
121
222
  generator: { _text: config.HOST_NAME },
@@ -182,6 +283,18 @@ module.exports = (config, axios, { raygunClient, publicPath } = {}) => ({
182
283
  ];
183
284
  }, [])
184
285
  };
286
+
287
+ return {
288
+ _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } },
289
+ rss: {
290
+ _attributes: { version: "2.0", 'xmlns:g': "http://base.google.com/ns/1.0" },
291
+ channel: {
292
+ lastBuildDate: { _text: new Date().toUTCString() },
293
+ docs: { _text: "https://validator.w3.org/feed/docs/rss2.html" },
294
+ ...channel
295
+ },
296
+ },
297
+ }
185
298
  }
186
299
  }],
187
300
  router: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lancom/shared",
3
- "version": "0.0.206",
3
+ "version": "0.0.208",
4
4
  "description": "lancom common scripts",
5
5
  "author": "e.tokovenko <e.tokovenko@gmail.com>",
6
6
  "repository": {