@kiva/kv-components 3.47.0 → 3.48.1

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.
package/vue/KvMap.vue CHANGED
@@ -13,6 +13,8 @@
13
13
  </template>
14
14
 
15
15
  <script>
16
+ import { animationCoordinator, generateMapMarkers } from '../utils/mapAnimation';
17
+
16
18
  export default {
17
19
  name: 'KvMap',
18
20
  props: {
@@ -87,6 +89,32 @@ export default {
87
89
  type: Number,
88
90
  default: 4,
89
91
  },
92
+ /**
93
+ * Borrower points object.
94
+ * If this object is present, the advanced animation will be triggered
95
+ * Sample object:
96
+ * {
97
+ borrowerPoints: [
98
+ {
99
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/e60a3d61ff052d60991c5d6bbf4a45d3.jpg',
100
+ location: [-77.032, 38.913],
101
+ },
102
+ {
103
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/6101929097c6e5de48232a4d1ae3b71c.jpg',
104
+ location: [41.402, 7.160],
105
+ },
106
+ {
107
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/11e018ee3d8b9c5adee459c16a29d264.jpg',
108
+ location: [-73.356596, 3.501],
109
+ },
110
+ ],
111
+ * }
112
+ */
113
+ advancedAnimation: {
114
+ type: Object,
115
+ required: false,
116
+ default: () => ({}),
117
+ },
90
118
  },
91
119
  data() {
92
120
  return {
@@ -174,10 +202,18 @@ export default {
174
202
  this.wrapperObserver = this.createIntersectionObserver({
175
203
  targets: [this.$refs?.[this.refString]],
176
204
  callback: (entries) => {
205
+ // only activate autoZoom if we have an initialZoom set
177
206
  entries.forEach((entry) => {
178
207
  if (entry.target === this.$refs?.[this.refString] && !this.zoomActive) {
179
208
  if (entry.intersectionRatio > 0) {
180
- this.activateZoom();
209
+ // activate zoom
210
+ if (this.initialZoom !== null) {
211
+ this.activateZoom();
212
+ }
213
+ // animate map
214
+ if (this.advancedAnimation?.borrowerPoints) {
215
+ this.animateMap();
216
+ }
181
217
  }
182
218
  }
183
219
  });
@@ -291,13 +327,40 @@ export default {
291
327
  dragRotate: false,
292
328
  });
293
329
 
294
- // signify map has loaded
295
- this.mapLoaded = true;
330
+ this.mapInstance.on('load', () => {
331
+ // signify map has loaded
332
+ this.mapLoaded = true;
333
+ // Create wrapper observer to watch for map entering viewport
334
+ if (this.initialZoom !== null || this.advancedAnimation?.borrowerPoints) {
335
+ this.createWrapperObserver();
336
+ }
337
+ });
338
+ },
339
+ animateMap() {
340
+ // remove country labels
341
+ this.mapInstance.style.stylesheet.layers.forEach((layer) => {
342
+ if (layer.type === 'symbol') {
343
+ this.mapInstance.removeLayer(layer.id);
344
+ }
345
+ });
346
+ // generate map markers for borrower points
347
+ generateMapMarkers(this.mapInstance, this.advancedAnimation.borrowerPoints);
296
348
 
297
- // only activate autoZoom if we have an initialZoom set
298
- if (this.initialZoom !== null) {
299
- this.createWrapperObserver();
300
- }
349
+ // wait 500 ms before calling the animation coordinator promise
350
+ // to allow the map to scroll into view
351
+ setTimeout(() => {
352
+ animationCoordinator(this.mapInstance, this.advancedAnimation.borrowerPoints)
353
+ .then(() => {
354
+ // when animation is complete reset map to component properties
355
+ this.mapInstance.dragPan.enable();
356
+ this.mapInstance.scrollZoom.enable();
357
+ this.mapInstance.scrollZoom.enable();
358
+ this.mapInstance.easeTo({
359
+ center: [this.long, this.lat],
360
+ zoom: this.initialZoom || this.zoomLevel,
361
+ });
362
+ });
363
+ }, 500);
301
364
  },
302
365
  checkIntersectionObserverSupport() {
303
366
  if (typeof window === 'undefined'
@@ -348,3 +411,40 @@ export default {
348
411
  },
349
412
  };
350
413
  </script>
414
+
415
+ <style>
416
+ /* Styles for animation map markers defined in @kiva/kv-components/utils/mapAnimation.js */
417
+ .map-marker {
418
+ margin-top: -77px;
419
+ margin-left: 35px;
420
+ display: block;
421
+ border: none;
422
+ border-radius: 50%;
423
+ cursor: pointer;
424
+ padding: 0;
425
+ }
426
+
427
+ .map-marker::after {
428
+ content: '';
429
+ position: absolute;
430
+ top: -8px;
431
+ left: -8px;
432
+ right: -8px;
433
+ bottom: -8px;
434
+ border-radius: 50%;
435
+ border: 4px solid #000;
436
+ }
437
+
438
+ .map-marker::before {
439
+ content: "";
440
+ width: 0;
441
+ height: 0;
442
+ left: -13px;
443
+ bottom: -32px;
444
+ border: 9px solid transparent;
445
+ border-left: 40px solid #000;
446
+ transform: rotate(114deg);
447
+ position: absolute;
448
+ z-index: -1;
449
+ }
450
+ </style>
@@ -0,0 +1,429 @@
1
+ <template>
2
+ <div
3
+ class="
4
+ tw-flex
5
+ tw-flex-row
6
+ tw-flex-wrap
7
+ tw-bg-white
8
+ tw-rounded
9
+ tw-w-full
10
+ tw-pb-1
11
+ tw-p-1
12
+ tw-gap-1
13
+ tw-items-center
14
+ "
15
+ :class="{'tw-pointer-events-none' : isLoading }"
16
+ style="box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);"
17
+ >
18
+ <div
19
+ class="loan-card-active-hover tw-flex-1 tw-min-w-[275px] md:tw-min-w-[320px] md:tw-max-w-[320px]"
20
+ >
21
+ <!-- Borrower image -->
22
+ <kv-loading-placeholder
23
+ v-if="isLoading"
24
+ class="tw-mb-1 tw-w-full tw-rounded"
25
+ :style="{ height: '15rem' }"
26
+ />
27
+ <div
28
+ v-else
29
+ class="tw-relative tw-w-full"
30
+ @click="showLoanDetails"
31
+ >
32
+ <component
33
+ :is="tag"
34
+ :to="readMorePath"
35
+ :href="readMorePath"
36
+ class="tw-flex"
37
+ aria-label="Borrower image"
38
+ @click="clickReadMore('Photo')"
39
+ >
40
+ <kv-borrower-image
41
+ class="
42
+ tw-relative
43
+ tw-w-full
44
+ tw-bg-black
45
+ tw-rounded
46
+ "
47
+ :alt="`Photo of ${borrowerName}`"
48
+ :aspect-ratio="imageAspectRatio"
49
+ :default-image="{ width: imageDefaultWidth }"
50
+ :hash="imageHash"
51
+ :images="imageSizes"
52
+ :photo-path="photoPath"
53
+ />
54
+
55
+ <div v-if="countryName">
56
+ <p
57
+ class="
58
+ tw-absolute
59
+ tw-bottom-1
60
+ tw-left-1
61
+ tw-text-primary
62
+ tw-bg-white
63
+ tw-rounded
64
+ tw-p-1
65
+ tw-mb-0
66
+ tw-mr-2
67
+ tw-text-h4
68
+ tw-inline-flex
69
+ tw-items-center"
70
+ style="padding: 2px 6px; text-transform: capitalize;"
71
+ >
72
+ <kv-material-icon
73
+ class="tw-h-2 tw-w-2"
74
+ :icon="mdiMapMarker"
75
+ />
76
+ {{ formattedLocation }}
77
+ </p>
78
+ </div>
79
+ </component>
80
+ <kv-loan-bookmark
81
+ v-if="!isVisitor"
82
+ :loan-id="loanId"
83
+ :is-bookmarked="isBookmarked"
84
+ class="tw-absolute tw-right-1"
85
+ style="top: -6px;"
86
+ data-testid="loan-card-bookmark"
87
+ @toggle-bookmark="$emit('toggle-bookmark')"
88
+ />
89
+ </div>
90
+ </div>
91
+
92
+ <div
93
+ class="tw-flex tw-flex-col tw-flex-1 tw-px-1"
94
+ >
95
+ <h3 class="tw-hidden md:tw-inline-block">
96
+ {{ borrowerName }}
97
+ </h3>
98
+ <!-- Loan tag -->
99
+ <component
100
+ :is="tag"
101
+ :to="readMorePath"
102
+ :href="readMorePath"
103
+ class="tw-flex hover:tw-no-underline focus:tw-no-underline"
104
+ aria-label="Loan tag"
105
+ @click="clickReadMore('Tag')"
106
+ >
107
+ <kv-loan-tag
108
+ v-if="showTags && !isLoading"
109
+ :loan="loan"
110
+ :kv-track-function="kvTrackFunction"
111
+ />
112
+ </component>
113
+
114
+ <component
115
+ :is="tag"
116
+ :to="readMorePath"
117
+ :href="readMorePath"
118
+ class="loan-card-use tw-text-primary"
119
+ aria-label="Loan use"
120
+ @click="clickReadMore('Use')"
121
+ >
122
+ <!-- Loan use -->
123
+ <div class="tw-pt-1">
124
+ <div
125
+ v-if="isLoading"
126
+ class="tw-w-full"
127
+ style="height: 5.5rem;"
128
+ >
129
+ <div
130
+ v-for="(_n, i) in [...Array(4)]"
131
+ :key="i"
132
+ class="tw-h-2 tw-mb-1"
133
+ >
134
+ <kv-loading-placeholder />
135
+ </div>
136
+ </div>
137
+ <div v-else>
138
+ <kv-loan-use
139
+ :use="loanUse"
140
+ :loan-amount="loanAmount"
141
+ :status="loanStatus"
142
+ :borrower-count="loanBorrowerCount"
143
+ :name="borrowerName"
144
+ :distribution-model="distributionModel"
145
+ />
146
+ </div>
147
+ </div>
148
+ </component>
149
+ <!-- Loan call outs -->
150
+ <kv-loading-placeholder
151
+ v-if="isLoading || typeof loanCallouts === 'undefined'"
152
+ class="tw-mt-1.5 tw-mb-1"
153
+ :style="{ width: '60%', height: '1.75rem', 'border-radius': '500rem' }"
154
+ />
155
+
156
+ <kv-loan-callouts
157
+ v-else
158
+ :callouts="loanCallouts"
159
+ class="loan-callouts tw-my-1.5"
160
+ />
161
+
162
+ <div class="tw-flex tw-flex-row tw-justify-between tw-gap-1 md:tw-gap-3 ">
163
+ <!-- Fundraising -->
164
+ <div
165
+ v-if="!hasProgressData"
166
+ class="tw-w-full tw-pt-1 tw-pr-1"
167
+ >
168
+ <kv-loading-placeholder
169
+ class="tw-mb-0.5"
170
+ :style="{ width: '70%', height: '1.3rem' }"
171
+ />
172
+
173
+ <kv-loading-placeholder
174
+ class="tw-rounded"
175
+ :style="{ width: '70%', height: '0.5rem' }"
176
+ />
177
+ </div>
178
+ <component
179
+ :is="tag"
180
+ v-if="unreservedAmount > 0"
181
+ :to="readMorePath"
182
+ :href="readMorePath"
183
+ class="loan-card-progress tw-mt-1 tw-flex-grow"
184
+ aria-label="Loan progress"
185
+ @click="clickReadMore('Progress')"
186
+ >
187
+ <kv-loan-progress-group
188
+ id="loanProgress"
189
+ :money-left="`${unreservedAmount}`"
190
+ :progress-percent="fundraisingPercent"
191
+ class="tw-text-black"
192
+ />
193
+ </component>
194
+
195
+ <!-- CTA Button -->
196
+ <kv-loading-placeholder
197
+ v-if="!allDataLoaded"
198
+ class="tw-rounded tw-self-start"
199
+ :style="{ width: '9rem', height: '3rem' }"
200
+ />
201
+
202
+ <kv-lend-cta
203
+ v-else
204
+ :loan="loan"
205
+ :basket-items="basketItems"
206
+ :is-loading="isLoading"
207
+ :is-adding="isAdding"
208
+ :enable-five-dollars-notes="enableFiveDollarsNotes"
209
+ :five-dollars-selected="fiveDollarsSelected"
210
+ :kv-track-function="kvTrackFunction"
211
+ :show-view-loan="showViewLoan"
212
+ :custom-loan-details="customLoanDetails"
213
+ :external-links="externalLinks"
214
+ :route="route"
215
+ :user-balance="userBalance"
216
+ :get-cookie="getCookie"
217
+ :set-cookie="setCookie"
218
+ class="tw-mt-auto tw-self-end"
219
+ :class="{'tw-flex-grow' : unreservedAmount === 0, 'tw-flex-shrink-0' : unreservedAmount > 0}"
220
+ @add-to-basket="$emit('add-to-basket', $event)"
221
+ @show-loan-details="clickReadMore('ViewLoan')"
222
+ />
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </template>
227
+
228
+ <script>
229
+ import { loanCardComputedProperties, loanCardMethods } from '../utils/loanCard';
230
+ import KvLoanUse from './KvLoanUse.vue';
231
+ import KvBorrowerImage from './KvBorrowerImage.vue';
232
+ import KvLoanProgressGroup from './KvLoanProgressGroup.vue';
233
+ import KvLoanCallouts from './KvLoanCallouts.vue';
234
+ import KvLendCta from './KvLendCta.vue';
235
+ import KvLoanBookmark from './KvLoanBookmark.vue';
236
+ import KvLoanTag from './KvLoanTag.vue';
237
+ import KvMaterialIcon from './KvMaterialIcon.vue';
238
+ import KvLoadingPlaceholder from './KvLoadingPlaceholder.vue';
239
+
240
+ export default {
241
+ name: 'KvWideLoanCard',
242
+ components: {
243
+ KvBorrowerImage,
244
+ KvLoadingPlaceholder,
245
+ KvLoanUse,
246
+ KvLoanProgressGroup,
247
+ KvMaterialIcon,
248
+ KvLendCta,
249
+ KvLoanTag,
250
+ KvLoanCallouts,
251
+ KvLoanBookmark,
252
+ },
253
+ props: {
254
+ loanId: {
255
+ type: Number,
256
+ default: undefined,
257
+ },
258
+ loan: {
259
+ type: Object,
260
+ default: null,
261
+ },
262
+ customLoanDetails: {
263
+ type: Boolean,
264
+ default: false,
265
+ },
266
+ showTags: {
267
+ type: Boolean,
268
+ default: false,
269
+ },
270
+ categoryPageName: {
271
+ type: String,
272
+ default: '',
273
+ },
274
+ enableFiveDollarsNotes: {
275
+ type: Boolean,
276
+ default: false,
277
+ },
278
+ isAdding: {
279
+ type: Boolean,
280
+ default: false,
281
+ },
282
+ isVisitor: {
283
+ type: Boolean,
284
+ default: true,
285
+ },
286
+ basketItems: {
287
+ type: Array,
288
+ default: () => ([]),
289
+ },
290
+ isBookmarked: {
291
+ type: Boolean,
292
+ default: false,
293
+ },
294
+ kvTrackFunction: {
295
+ type: Function,
296
+ required: true,
297
+ },
298
+ photoPath: {
299
+ type: String,
300
+ required: true,
301
+ },
302
+ showViewLoan: {
303
+ type: Boolean,
304
+ default: false,
305
+ },
306
+ externalLinks: {
307
+ type: Boolean,
308
+ default: false,
309
+ },
310
+ route: {
311
+ type: Object,
312
+ default: undefined,
313
+ },
314
+ userBalance: {
315
+ type: String,
316
+ default: undefined,
317
+ },
318
+ getCookie: {
319
+ type: Function,
320
+ default: undefined,
321
+ },
322
+ setCookie: {
323
+ type: Function,
324
+ default: undefined,
325
+ },
326
+ fiveDollarsSelected: {
327
+ type: Boolean,
328
+ default: false,
329
+ },
330
+ customCallouts: {
331
+ type: Array,
332
+ default: () => ([]),
333
+ },
334
+ },
335
+ setup(props) {
336
+ const {
337
+ allDataLoaded,
338
+ borrowerName,
339
+ city,
340
+ countryName,
341
+ distributionModel,
342
+ formattedLocation,
343
+ fundraisingPercent,
344
+ hasProgressData,
345
+ imageHash,
346
+ isLoading,
347
+ loanAmount,
348
+ loanBorrowerCount,
349
+ loanCallouts,
350
+ loanStatus,
351
+ loanUse,
352
+ mdiMapMarker,
353
+ readMorePath,
354
+ state,
355
+ tag,
356
+ unreservedAmount,
357
+ } = loanCardComputedProperties(props);
358
+
359
+ const {
360
+ clickReadMore,
361
+ showLoanDetails,
362
+ } = loanCardMethods(props);
363
+
364
+ return {
365
+ allDataLoaded,
366
+ borrowerName,
367
+ city,
368
+ countryName,
369
+ distributionModel,
370
+ formattedLocation,
371
+ fundraisingPercent,
372
+ hasProgressData,
373
+ imageHash,
374
+ isLoading,
375
+ loanAmount,
376
+ loanBorrowerCount,
377
+ loanCallouts,
378
+ loanStatus,
379
+ loanUse,
380
+ mdiMapMarker,
381
+ readMorePath,
382
+ state,
383
+ tag,
384
+ unreservedAmount,
385
+ clickReadMore,
386
+ showLoanDetails,
387
+ };
388
+ },
389
+ computed: {
390
+ cardWidth() {
391
+ return '374px';
392
+ },
393
+ imageAspectRatio() {
394
+ return 1;
395
+ },
396
+ imageDefaultWidth() {
397
+ return 320;
398
+ },
399
+ imageSizes() {
400
+ return [
401
+ { width: this.imageDefaultWidth, viewSize: 1024 },
402
+ { width: this.imageDefaultWidth, viewSize: 768 },
403
+ { width: 416, viewSize: 480 },
404
+ { width: 374, viewSize: 414 },
405
+ { width: 335, viewSize: 375 },
406
+ ];
407
+ },
408
+ },
409
+ };
410
+ </script>
411
+
412
+ <style lang="postcss" scoped>
413
+ /** Shared with KvClassicLoanCard */
414
+ .loan-card-use:hover,
415
+ .loan-card-use:focus {
416
+ @apply tw-text-primary;
417
+ }
418
+ .loan-card-active-hover:hover .loan-card-use {
419
+ @apply tw-underline;
420
+ }
421
+ .loan-card-progress:hover,
422
+ .loan-card-progress:focus {
423
+ @apply tw-no-underline;
424
+ }
425
+ /** Unique to this loan card */
426
+ .loan-callouts >>> div{
427
+ @apply tw-flex-wrap tw-h-auto;
428
+ }
429
+ </style>
@@ -60,3 +60,9 @@ const props = {
60
60
  };
61
61
 
62
62
  export const Default = story(props);
63
+
64
+ export const Square = story({
65
+ ...props,
66
+ aspectRatio: 1,
67
+ defaultImage: { width: 300 },
68
+ });
@@ -28,6 +28,7 @@ const story = (args) => {
28
28
  :kv-track-function="kvTrackFunction"
29
29
  :photo-path="photoPath"
30
30
  :show-view-loan="showViewLoan"
31
+ :custom-callouts="customCallouts"
31
32
  />
32
33
  </div>
33
34
  `,
@@ -118,6 +119,38 @@ export const ShowTags = story({
118
119
  showTags: true,
119
120
  });
120
121
 
122
+ export const CustomCallouts = story({
123
+ loanId: loan.id,
124
+ loan: {
125
+ ...loan,
126
+ loanFundraisingInfo: {
127
+ fundedAmount: '950.00',
128
+ isExpiringSoon: false,
129
+ reservedAmount: '0.00',
130
+ },
131
+ },
132
+ kvTrackFunction,
133
+ photoPath,
134
+ showTags: true,
135
+ customCallouts: ['Loan Length: 15mo'],
136
+ });
137
+
138
+ export const CustomCalloutsWrap = story({
139
+ loanId: loan.id,
140
+ loan: {
141
+ ...loan,
142
+ loanFundraisingInfo: {
143
+ fundedAmount: '950.00',
144
+ isExpiringSoon: false,
145
+ reservedAmount: '0.00',
146
+ },
147
+ },
148
+ kvTrackFunction,
149
+ photoPath,
150
+ showTags: true,
151
+ customCallouts: ['Loan Length: 15mo', 'Long Dairy Processing'],
152
+ });
153
+
121
154
  export const Matched = story({
122
155
  loanId: loan.id,
123
156
  loan: {
@@ -13,6 +13,7 @@ export default {
13
13
  useLeaflet: false,
14
14
  width: null,
15
15
  zoomLevel: 4,
16
+ advancedAnimation: {},
16
17
  },
17
18
  };
18
19
 
@@ -30,6 +31,7 @@ const Template = (args, { argTypes }) => ({
30
31
  :use-leaflet="useLeaflet"
31
32
  :width="width"
32
33
  :zoom-level="zoomLevel"
34
+ :advanced-animation="advancedAnimation"
33
35
  />`,
34
36
  });
35
37
 
@@ -67,3 +69,32 @@ Leaflet.args = {
67
69
  useLeaflet: true,
68
70
  zoomLevel: 6,
69
71
  };
72
+
73
+ export const AdvancedAnimation = Template.bind({});
74
+ const advancedAnimation = {
75
+ borrowerPoints: [
76
+ {
77
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/e60a3d61ff052d60991c5d6bbf4a45d3.jpg',
78
+ location: [-77.032, 38.913],
79
+ },
80
+ {
81
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/6101929097c6e5de48232a4d1ae3b71c.jpg',
82
+ location: [41.402, 7.160],
83
+ },
84
+ {
85
+ image: 'https://www-kiva-org.freetls.fastly.net/img/w80h80fz50/11e018ee3d8b9c5adee459c16a29d264.jpg',
86
+ location: [-73.356596, 3.501],
87
+ },
88
+ ],
89
+ };
90
+ AdvancedAnimation.args = {
91
+ initialZoom: null,
92
+ mapId: 5,
93
+ useLeaflet: false,
94
+ zoomLevel: 2,
95
+ height: 600,
96
+ width: 1000,
97
+ lat: 21.096,
98
+ long: -31.690,
99
+ advancedAnimation,
100
+ };