@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.
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Code to generate random coordinates
3
+ * */
4
+ function getRandomInRange(from, to, fixed) {
5
+ return (Math.random() * (to - from) + from).toFixed(fixed) * 1;
6
+ // .toFixed() returns string, so ' * 1' is a trick to convert to number
7
+ }
8
+ const randomCoordinates = Array.from(
9
+ { length: 20 },
10
+ () => [getRandomInRange(-180, 180, 3), getRandomInRange(-90, 90, 3)],
11
+ );
12
+
13
+ /**
14
+ * Given 2 coordinates and the number of steps return an array of coordinates in between
15
+ * @param {Array} startCoordinates - starting coordinates in the format [latitude, longitude]
16
+ * @param {Array} endCoordinates - ending coordinates in the format [latitude, longitude]
17
+ * @param {Number} numberOfSteps - number of steps to take between the start and end coordinates
18
+ * @returns {Array} - array of coordinates in the format [[latitude, longitude], [latitude, longitude]]
19
+ */
20
+ export function getCoordinatesBetween(startCoordinates, endCoordinates, numberOfSteps) {
21
+ // All invalid inputs should return an empty array
22
+ if (!startCoordinates
23
+ || !endCoordinates
24
+ || !numberOfSteps
25
+ || numberOfSteps < 1
26
+ || !Array.isArray(startCoordinates)
27
+ || !Array.isArray(endCoordinates)
28
+ || startCoordinates.length !== 2 || endCoordinates.length !== 2) {
29
+ return [];
30
+ }
31
+ const diffX = endCoordinates[0] - startCoordinates[0];
32
+ const diffY = endCoordinates[1] - startCoordinates[1];
33
+
34
+ const sfX = diffX / numberOfSteps;
35
+ const sfY = diffY / numberOfSteps;
36
+
37
+ let i = 0;
38
+ let j = 0;
39
+
40
+ const lineCoordinates = [];
41
+
42
+ while (Math.abs(i) < Math.abs(diffX) || Math.abs(j) < Math.abs(diffY)) {
43
+ lineCoordinates.push([startCoordinates[0] + i, startCoordinates[1] + j]);
44
+
45
+ if (Math.abs(i) < Math.abs(diffX)) {
46
+ i += sfX;
47
+ }
48
+
49
+ if (Math.abs(j) < Math.abs(diffY)) {
50
+ j += sfY;
51
+ }
52
+ }
53
+ // Because of rounding errors, lets push the exact end coordinates
54
+ // as the last item in the array to make sure the line ends precisely
55
+ lineCoordinates[lineCoordinates.length - 1] = [endCoordinates[0], endCoordinates[1]];
56
+ return lineCoordinates;
57
+ }
58
+
59
+ /**
60
+ * This function animates a series of lines from an array of starting coordinates to a single end point
61
+ * then animates removing the line from the origin to the end point
62
+ * returns a promise when the animation is complete
63
+ * @param {Map Instance} mapInstance - the map instance
64
+ * @param {Array} originPoints - array of starting coordinates in the format [[latitude, longitude], [latitude, longitude]]
65
+ * @param {Array} endPoint - single end point in the format [latitude, longitude]
66
+ * @returns {Promise} - promise that resolves when the animation is complete
67
+ */
68
+ function animateLines(mapInstance, originPoints, endPoint) {
69
+ const speedFactor = 100; // number of frames per degree, controls animation speed
70
+ return new Promise((resolve) => {
71
+ // EndPoint
72
+ mapInstance.addSource('endpoint', {
73
+ type: 'geojson',
74
+ data: {
75
+ type: 'Point',
76
+ coordinates: [
77
+ endPoint[0], endPoint[1],
78
+ ],
79
+ },
80
+ });
81
+
82
+ const lineFlight = (startCoordinates, endCoordinates, index, lastLine = false) => {
83
+ const lineCoordinates = getCoordinatesBetween(startCoordinates, endCoordinates, speedFactor);
84
+ let animationCounter = 0;
85
+
86
+ // Create a GeoJSON source with an empty lineString.
87
+ const geojson = {
88
+ type: 'FeatureCollection',
89
+ features: [{
90
+ type: 'Feature',
91
+ geometry: {
92
+ type: 'LineString',
93
+ coordinates: [],
94
+ },
95
+ }],
96
+ };
97
+
98
+ // Start Point
99
+ mapInstance.addSource(`startPoint${index}`, {
100
+ type: 'geojson',
101
+ data: {
102
+ type: 'Point',
103
+ coordinates: [
104
+ startCoordinates[0], startCoordinates[1],
105
+ ],
106
+ },
107
+ });
108
+
109
+ // Line
110
+ mapInstance.addLayer({
111
+ id: `line-animation${index}`,
112
+ type: 'line',
113
+ source: {
114
+ type: 'geojson',
115
+ data: geojson,
116
+ },
117
+ layout: {
118
+ 'line-cap': 'round',
119
+ 'line-join': 'round',
120
+ },
121
+ paint: {
122
+ 'line-color': '#277056',
123
+ 'line-width': 2,
124
+ },
125
+ });
126
+
127
+ const animateLine = () => {
128
+ if (animationCounter < lineCoordinates.length) {
129
+ geojson.features[0].geometry.coordinates.push(lineCoordinates[animationCounter]);
130
+ mapInstance.getSource(`line-animation${index}`).setData(geojson);
131
+
132
+ requestAnimationFrame(animateLine);
133
+ animationCounter += 1;
134
+ } else {
135
+ // This else block is for animating line removal from start to end
136
+ const coord = geojson.features[0].geometry.coordinates;
137
+ // remove 2 points at a time so the removal is twice as fast as the line creation
138
+ coord.shift();
139
+ coord.shift();
140
+
141
+ if (coord.length > 0) {
142
+ geojson.features[0].geometry.coordinates = coord;
143
+ mapInstance.getSource(`line-animation${index}`).setData(geojson);
144
+ requestAnimationFrame(animateLine);
145
+ } else {
146
+ // remove all sources to allow for new animation
147
+ mapInstance.removeLayer(`line-animation${index}`);
148
+ mapInstance.removeSource(`line-animation${index}`);
149
+ mapInstance.removeSource(`startPoint${index}`);
150
+
151
+ if (lastLine) {
152
+ mapInstance.removeSource('endpoint');
153
+ resolve();
154
+ }
155
+ }
156
+ }
157
+ };
158
+
159
+ animateLine();
160
+ };
161
+
162
+ originPoints.forEach((coordinate, index) => {
163
+ lineFlight(coordinate, endPoint, index, index === originPoints.length - 1);
164
+ });
165
+ });
166
+ }
167
+
168
+ /**
169
+ * This function generates a map marker for each borrowerPoint
170
+ * @param {Map Instance} mapInstance - the map instance
171
+ * @param {Array} borrowerPoints - array of borrower objects
172
+ * @returns {void}
173
+ * */
174
+ export function generateMapMarkers(mapInstance, borrowerPoints) {
175
+ const geojson = {
176
+ type: 'FeatureCollection',
177
+ };
178
+
179
+ geojson.features = borrowerPoints.map((borrower) => ({
180
+ type: 'Feature',
181
+ properties: {
182
+ message: 'test',
183
+ image: borrower.image,
184
+ iconSize: [80, 80],
185
+ },
186
+ geometry: {
187
+ type: 'Point',
188
+ coordinates: borrower.location,
189
+ },
190
+ }));
191
+ // add markers to map
192
+ geojson.features.forEach((marker) => {
193
+ // create a DOM element for the marker
194
+ const el = document.createElement('div');
195
+ el.className = 'map-marker';
196
+ el.style.backgroundImage = `url(${marker.properties.image})`;
197
+ el.style.width = `${marker.properties.iconSize[0]}px`;
198
+ el.style.height = `${marker.properties.iconSize[1]}px`;
199
+
200
+ // Possible place to add an event listener
201
+ // el.addEventListener('click', () => {
202
+ // window.alert(marker.properties.message);
203
+ // });
204
+
205
+ // add marker to map
206
+ // maplibregl should be defined in the KvMap component
207
+ // eslint-disable-next-line no-undef
208
+ new maplibregl.Marker({ element: el })
209
+ .setLngLat(marker.geometry.coordinates)
210
+ .addTo(mapInstance);
211
+ });
212
+ }
213
+
214
+ /**
215
+ * This function animates a series of lines from an array of starting coordinates to a single end point
216
+ * then animates removing the line from the origin to the end point
217
+ * then flies to the next point in the array and repeats the animation
218
+ * returns a promise when the animation is complete
219
+ * @param {Map Instance} mapInstance - the map instance
220
+ * @param {Array} borrowerPoints - array of borrower objects
221
+ * @returns {Promise} - promise that resolves when the animation is complete
222
+ * */
223
+ export function animationCoordinator(mapInstance, borrowerPoints) {
224
+ return new Promise((resolve) => {
225
+ const destinationPoints = borrowerPoints.map((borrower) => borrower.location);
226
+ const totalNumberOfPoints = destinationPoints.length;
227
+ let currentPointIndex = 0;
228
+
229
+ const flyToPoint = (index) => {
230
+ mapInstance.flyTo({
231
+ // These options control the ending camera position: centered at
232
+ // the target, at zoom level 9, and north up.
233
+ center: destinationPoints[index],
234
+ zoom: 4,
235
+ bearing: 0,
236
+
237
+ // These options control the flight curve, making it move
238
+ // slowly and zoom out almost completely before starting
239
+ // to pan.
240
+ speed: 0.9, // make the flying slow
241
+ curve: 1, // change the speed at which it zooms out
242
+
243
+ // This can be any easing function: it takes a number between
244
+ // 0 and 1 and returns another number between 0 and 1.
245
+ easing(t) {
246
+ return t;
247
+ },
248
+
249
+ // this animation is considered essential with respect to prefers-reduced-motion
250
+ essential: true,
251
+ }, { flyEnd: true });
252
+ };
253
+
254
+ // This will trigger the next steps in the animation chain
255
+ mapInstance.on('moveend', (event) => {
256
+ if (event.flyEnd === true) {
257
+ animateLines(mapInstance, randomCoordinates, destinationPoints[currentPointIndex])
258
+ .then(() => {
259
+ if (currentPointIndex < totalNumberOfPoints - 1) {
260
+ currentPointIndex += 1;
261
+ flyToPoint(currentPointIndex);
262
+ } else {
263
+ resolve();
264
+ }
265
+ });
266
+ }
267
+ });
268
+
269
+ // fly to point 1
270
+ flyToPoint(currentPointIndex);
271
+ });
272
+ }
@@ -146,8 +146,17 @@ export default {
146
146
  if (!width && !height && !square && !faceZoom) {
147
147
  return '';
148
148
  }
149
- const w = width ? `w${Math.ceil(width)}` : '';
150
- const h = height ? `h${Math.ceil(height)}` : '';
149
+ let w = '';
150
+ let h = '';
151
+ // If width and height are different, use w and h
152
+ if (width === height) {
153
+ // if height and width are the same, use square param
154
+ // eslint-disable-next-line no-param-reassign
155
+ square = width;
156
+ } else {
157
+ w = width ? `w${Math.ceil(width)}` : '';
158
+ h = height ? `h${Math.ceil(height)}` : '';
159
+ }
151
160
  const s = square ? `s${Math.ceil(square)}` : '';
152
161
  const fz = faceZoom ? `fz${Math.ceil(faceZoom)}` : '';
153
162
 
@@ -181,7 +181,7 @@
181
181
  >
182
182
  <kv-loan-progress-group
183
183
  id="loanProgress"
184
- :money-left="unreservedAmount"
184
+ :money-left="`${unreservedAmount}`"
185
185
  :progress-percent="fundraisingPercent"
186
186
  class="tw-text-black"
187
187
  />
@@ -220,7 +220,8 @@
220
220
  </template>
221
221
 
222
222
  <script>
223
- import { mdiMapMarker } from '@mdi/js';
223
+ import { loanCardComputedProperties, loanCardMethods } from '../utils/loanCard';
224
+
224
225
  import KvLoanUse from './KvLoanUse.vue';
225
226
  import KvBorrowerImage from './KvBorrowerImage.vue';
226
227
  import KvLoanProgressGroup from './KvLoanProgressGroup.vue';
@@ -231,12 +232,6 @@ import KvLoanTag from './KvLoanTag.vue';
231
232
  import KvMaterialIcon from './KvMaterialIcon.vue';
232
233
  import KvLoadingPlaceholder from './KvLoadingPlaceholder.vue';
233
234
 
234
- const LSE_LOAN_KEY = 'N/A';
235
- const ECO_FRIENDLY_KEY = 'ECO-FRIENDLY';
236
- const SUSTAINABLE_AG_KEY = 'SUSTAINABLE AG';
237
- const SINGLE_PARENT_KEY = 'SINGLE PARENT';
238
- const REFUGEE_KEY = 'REFUGEES/DISPLACED';
239
-
240
235
  export default {
241
236
  name: 'KvClassicLoanCard',
242
237
  components: {
@@ -263,10 +258,6 @@ export default {
263
258
  type: Boolean,
264
259
  default: false,
265
260
  },
266
- useFullWidth: {
267
- type: Boolean,
268
- default: false,
269
- },
270
261
  showTags: {
271
262
  type: Boolean,
272
263
  default: false,
@@ -279,10 +270,6 @@ export default {
279
270
  type: Boolean,
280
271
  default: false,
281
272
  },
282
- largeCard: {
283
- type: Boolean,
284
- default: false,
285
- },
286
273
  isAdding: {
287
274
  type: Boolean,
288
275
  default: false,
@@ -335,80 +322,77 @@ export default {
335
322
  type: Boolean,
336
323
  default: false,
337
324
  },
325
+ customCallouts: {
326
+ type: Array,
327
+ default: () => ([]),
328
+ },
329
+ useFullWidth: {
330
+ type: Boolean,
331
+ default: false,
332
+ },
333
+ largeCard: {
334
+ type: Boolean,
335
+ default: false,
336
+ },
338
337
  },
339
- data() {
338
+ setup(props) {
339
+ const {
340
+ allDataLoaded,
341
+ borrowerName,
342
+ city,
343
+ countryName,
344
+ distributionModel,
345
+ formattedLocation,
346
+ fundraisingPercent,
347
+ hasProgressData,
348
+ imageHash,
349
+ isLoading,
350
+ loanAmount,
351
+ loanBorrowerCount,
352
+ loanCallouts,
353
+ loanStatus,
354
+ loanUse,
355
+ mdiMapMarker,
356
+ readMorePath,
357
+ state,
358
+ tag,
359
+ unreservedAmount,
360
+ } = loanCardComputedProperties(props);
361
+
362
+ const {
363
+ clickReadMore,
364
+ showLoanDetails,
365
+ } = loanCardMethods(props);
366
+
340
367
  return {
368
+ allDataLoaded,
369
+ borrowerName,
370
+ city,
371
+ countryName,
372
+ distributionModel,
373
+ formattedLocation,
374
+ fundraisingPercent,
375
+ hasProgressData,
376
+ imageHash,
377
+ isLoading,
378
+ loanAmount,
379
+ loanBorrowerCount,
380
+ loanCallouts,
381
+ loanStatus,
382
+ loanUse,
341
383
  mdiMapMarker,
384
+ readMorePath,
385
+ state,
386
+ tag,
387
+ unreservedAmount,
388
+ clickReadMore,
389
+ showLoanDetails,
342
390
  };
343
391
  },
344
392
  computed: {
345
- tag() {
346
- return this.externalLinks ? 'a' : 'router-link';
347
- },
348
- readMorePath() {
349
- return this.customLoanDetails ? '' : `/lend/${this.loanId}`;
350
- },
351
- isLoading() {
352
- return !this.loanId || !this.loan;
353
- },
354
393
  cardWidth() {
355
394
  return this.useFullWidth ? '100%' : '374px';
356
395
  },
357
- borrowerName() {
358
- return this.loan?.name || '';
359
- },
360
- countryName() {
361
- return this.loan?.geocode?.country?.name || '';
362
- },
363
- city() {
364
- return this.loan?.geocode?.city || '';
365
- },
366
- state() {
367
- return this.loan?.geocode?.state || '';
368
- },
369
- distributionModel() {
370
- return this.loan?.distributionModel || '';
371
- },
372
- imageHash() {
373
- return this.loan?.image?.hash ?? '';
374
- },
375
- hasProgressData() {
376
- // Local resolver values for the progress bar load client-side
377
- return typeof this.loan?.unreservedAmount !== 'undefined'
378
- && typeof this.loan?.fundraisingPercent !== 'undefined';
379
- },
380
- allDataLoaded() {
381
- return !this.isLoading && this.hasProgressData;
382
- },
383
- fundraisingPercent() {
384
- return this.loan?.fundraisingPercent ?? 0;
385
- },
386
- unreservedAmount() {
387
- return this.loan?.unreservedAmount ?? '0';
388
- },
389
- formattedLocation() {
390
- if (this.distributionModel === 'direct') {
391
- const formattedString = `${this.city}, ${this.state}, ${this.countryName}`;
392
- return formattedString;
393
- }
394
- if (this.countryName === 'Puerto Rico') {
395
- const formattedString = `${this.city}, PR`;
396
- return formattedString;
397
- }
398
- return this.countryName;
399
- },
400
- loanUse() {
401
- return this.loan?.use ?? '';
402
- },
403
- loanStatus() {
404
- return this.loan?.status ?? '';
405
- },
406
- loanAmount() {
407
- return this.loan?.loanAmount ?? '0';
408
- },
409
- loanBorrowerCount() {
410
- return this.loan?.borrowerCount ?? 0;
411
- },
412
396
  imageAspectRatio() {
413
397
  if (this.largeCard) {
414
398
  return 5 / 8;
@@ -430,95 +414,12 @@ export default {
430
414
  { width: 335, viewSize: 375 },
431
415
  ];
432
416
  },
433
- loanCallouts() {
434
- const callouts = [];
435
- const activityName = this.loan?.activity?.name ?? '';
436
- const sectorName = this.loan?.sector?.name ?? '';
437
- const tags = this.loan?.tags?.filter((tag) => tag.charAt(0) === '#')
438
- .map((tag) => tag.substring(1)) ?? [];
439
- const themes = this.loan?.themes ?? [];
440
- const categories = {
441
- ecoFriendly: !!tags // eslint-disable-next-line max-len
442
- .filter((t) => t.toUpperCase() === ECO_FRIENDLY_KEY || t.toUpperCase() === SUSTAINABLE_AG_KEY).length,
443
- refugeesIdps: !!themes.filter((t) => t.toUpperCase() === REFUGEE_KEY).length,
444
- singleParents: !!tags.filter((t) => t.toUpperCase() === SINGLE_PARENT_KEY).length,
445
- };
446
-
447
- const isLseLoan = this.loan?.partnerName?.toUpperCase().includes(LSE_LOAN_KEY);
448
-
449
- // P1 Category
450
- // Exp limited to: Eco-friendly, Refugees and IDPs, Single Parents
451
- // Tag as first option for LSE loans
452
- if (isLseLoan && tags.length) {
453
- const position = Math.floor(Math.random() * tags.length);
454
- const tag = tags[position];
455
- callouts.push(tag);
456
- }
457
-
458
- if (!this.categoryPageName) {
459
- if (categories.ecoFriendly // eslint-disable-next-line max-len
460
- && !callouts.find((c) => c.toUpperCase() === ECO_FRIENDLY_KEY || c.toUpperCase() === SUSTAINABLE_AG_KEY)) {
461
- callouts.push('Eco-friendly');
462
- } else if (categories.refugeesIdps) {
463
- callouts.push('Refugees and IDPs');
464
- } else if (categories.singleParents
465
- && !callouts.find((c) => c.toUpperCase() === SINGLE_PARENT_KEY)) {
466
- callouts.push('Single Parent');
467
- }
468
- }
469
-
470
- // P2 Activity
471
- if (activityName && this.categoryPageName?.toUpperCase() !== activityName.toUpperCase()) {
472
- callouts.push(activityName);
473
- }
474
-
475
- // P3 Sector
476
- if (sectorName
477
- && (activityName.toUpperCase() !== sectorName.toUpperCase())
478
- && (sectorName.toUpperCase() !== this.categoryPageName?.toUpperCase())
479
- && callouts.length < 2) {
480
- callouts.push(sectorName);
481
- }
482
-
483
- // P4 Tag
484
- if (!!tags.length && callouts.length < 2) {
485
- const position = Math.floor(Math.random() * tags.length);
486
- const tag = tags[position];
487
- if (!callouts.filter((c) => c.toUpperCase() === tag.toUpperCase()).length) {
488
- callouts.push(tag);
489
- }
490
- }
491
-
492
- // P5 Theme
493
- if (!!themes.length && callouts.length < 2) {
494
- const position = Math.floor(Math.random() * themes.length);
495
- const theme = themes[position];
496
- if (!callouts.filter((c) => c.toUpperCase() === theme.toUpperCase()).length
497
- && theme.toUpperCase() !== this.categoryPageName?.toUpperCase()) {
498
- callouts.push(theme);
499
- }
500
- }
501
-
502
- // Only show one callout for LSE loans
503
- if (isLseLoan && callouts.length > 1) return [callouts.shift()];
504
- return callouts;
505
- },
506
- },
507
- methods: {
508
- showLoanDetails(e) {
509
- if (this.customLoanDetails) {
510
- e.preventDefault();
511
- this.$emit('show-loan-details');
512
- }
513
- },
514
- clickReadMore(target) {
515
- this.kvTrackFunction('Lending', 'click-Read more', target, this.loanId);
516
- },
517
417
  },
518
418
  };
519
419
  </script>
520
420
 
521
421
  <style lang="postcss" scoped>
422
+ /** Shared with KvWideLoanCard */
522
423
  .loan-card-use:hover,
523
424
  .loan-card-use:focus {
524
425
  @apply tw-text-primary;