@kiva/kv-components 3.15.0 → 3.17.0

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,462 @@
1
+ <template>
2
+ <div
3
+ :id="`${loanId}-loan-card`"
4
+ class="tw-flex tw-flex-col tw-bg-white tw-rounded tw-w-full tw-pb-1"
5
+ :class="{ 'tw-p-1': !largeCard }"
6
+ style="box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);"
7
+ :style="{ minWidth: '230px', maxWidth: cardWidth }"
8
+ >
9
+ <div class="tw-grow">
10
+ <div class="loan-card-active-hover">
11
+ <!-- Borrower image -->
12
+ <kv-loading-placeholder
13
+ v-if="isLoading"
14
+ class="tw-mb-1 tw-w-full"
15
+ :class="{ 'tw-rounded-t tw-rounded-b-none': largeCard, 'tw-rounded': !largeCard }"
16
+ :style="{ height: '15rem' }"
17
+ />
18
+ <div
19
+ v-else
20
+ class="tw-relative"
21
+ @click="showLoanDetails"
22
+ >
23
+ <kv-loan-bookmark
24
+ v-if="!isVisitor"
25
+ :loan-id="loanId"
26
+ :is-bookmarked="isBookmarked"
27
+ class="tw-absolute tw-right-1 tw-z-2"
28
+ style="top: -6px;"
29
+ data-testid="loan-card-bookmark"
30
+ @toggle-bookmark="$emit('toggle-bookmark')"
31
+ />
32
+ <router-link
33
+ :to="customLoanDetails ? '' : `/lend/${loanId}`"
34
+ class="tw-flex"
35
+ aria-label="Borrower image"
36
+ @click="clickReadMore('Photo')"
37
+ >
38
+ <kv-borrower-image
39
+ class="
40
+ tw-relative
41
+ tw-w-full
42
+ tw-bg-black
43
+ "
44
+ :class="{ 'tw-rounded-t': largeCard, 'tw-rounded': !largeCard }"
45
+ :alt="`Photo of ${borrowerName}`"
46
+ :aspect-ratio="imageAspectRatio"
47
+ :default-image="{ width: imageDefaultWidth }"
48
+ :hash="imageHash"
49
+ :images="imageSizes"
50
+ :photo-path="photoPath"
51
+ />
52
+
53
+ <div v-if="countryName">
54
+ <p
55
+ class="
56
+ tw-absolute
57
+ tw-bottom-1
58
+ tw-left-1
59
+ tw-text-primary
60
+ tw-bg-white
61
+ tw-rounded
62
+ tw-p-1
63
+ tw-mb-0
64
+ tw-mr-2
65
+ tw-text-h4
66
+ tw-inline-flex
67
+ tw-items-center"
68
+ style="padding: 2px 6px; text-transform: capitalize;"
69
+ >
70
+ <kv-material-icon
71
+ class="tw-h-2 tw-w-2"
72
+ :icon="mdiMapMarker"
73
+ />
74
+ {{ formattedLocation }}
75
+ </p>
76
+ </div>
77
+ </router-link>
78
+ </div>
79
+
80
+ <!-- Loan tag -->
81
+ <router-link
82
+ :to="customLoanDetails ? '' : `/lend/${loanId}`"
83
+ class="tw-flex hover:tw-no-underline focus:tw-no-underline"
84
+ :class="{ 'tw-px-1': largeCard }"
85
+ aria-label="Loan tag"
86
+ @click="clickReadMore('Tag')"
87
+ >
88
+ <kv-loan-tag
89
+ v-if="showTags && !isLoading"
90
+ :loan="loan"
91
+ :kv-track-function="kvTrackFunction"
92
+ />
93
+ </router-link>
94
+
95
+ <router-link
96
+ :to="customLoanDetails ? '' : `/lend/${loanId}`"
97
+ class="loan-card-use tw-text-primary"
98
+ aria-label="Loan use"
99
+ @click="clickReadMore('Use')"
100
+ >
101
+ <!-- Loan use -->
102
+ <div class="tw-mb-1.5 tw-pt-1">
103
+ <div
104
+ v-if="isLoading"
105
+ class="tw-w-full"
106
+ :class="{ 'tw-px-1': largeCard }"
107
+ style="height: 5.5rem;"
108
+ >
109
+ <kv-loading-placeholder
110
+ v-for="(_n, i) in [...Array(4)]"
111
+ :key="i"
112
+ class="tw-h-2 tw-mb-1 tw-w-1.5"
113
+ />
114
+ </div>
115
+ <div v-else>
116
+ <kv-loan-use
117
+ :use="loanUse"
118
+ :loan-amount="loanAmount"
119
+ :status="loanStatus"
120
+ :borrower-count="loanBorrowerCount"
121
+ :name="borrowerName"
122
+ :distribution-model="distributionModel"
123
+ :class="{ 'tw-px-1': largeCard }"
124
+ />
125
+ </div>
126
+ </div>
127
+ </router-link>
128
+ </div>
129
+
130
+ <!-- Loan call outs -->
131
+ <kv-loading-placeholder
132
+ v-if="isLoading || typeof loanCallouts === 'undefined'"
133
+ class="tw-mt-1.5 tw-mb-1"
134
+ :class="{ 'tw-mx-1': largeCard }"
135
+ :style="{ width: '60%', height: '1.75rem', 'border-radius': '500rem' }"
136
+ />
137
+
138
+ <kv-loan-callouts
139
+ v-else
140
+ :callouts="loanCallouts"
141
+ class="tw-mt-1.5"
142
+ :class="{ 'tw-px-1': largeCard }"
143
+ />
144
+ </div>
145
+
146
+ <div
147
+ class="tw-flex tw-justify-between tw-mt-2"
148
+ :class="{ 'tw-px-1': largeCard }"
149
+ >
150
+ <!-- Fundraising -->
151
+ <div
152
+ v-if="!hasProgressData"
153
+ class="tw-w-full tw-pt-1 tw-pr-1"
154
+ >
155
+ <kv-loading-placeholder
156
+ class="tw-mb-0.5"
157
+ :style="{ width: '70%', height: '1.3rem' }"
158
+ />
159
+
160
+ <kv-loading-placeholder
161
+ class="tw-rounded"
162
+ :style="{ width: '70%', height: '0.5rem' }"
163
+ />
164
+ </div>
165
+
166
+ <router-link
167
+ v-if="unreservedAmount > 0"
168
+ :to="customLoanDetails ? '' : `/lend/${loanId}`"
169
+ class="loan-card-progress tw-mt-1"
170
+ aria-label="Loan progress"
171
+ @click="clickReadMore('Progress')"
172
+ >
173
+ <kv-loan-progress-group
174
+ id="loanProgress"
175
+ :money-left="unreservedAmount"
176
+ :progress-percent="fundraisingPercent"
177
+ class="tw-text-black"
178
+ />
179
+ </router-link>
180
+
181
+ <!-- CTA Button -->
182
+ <kv-loading-placeholder
183
+ v-if="!allDataLoaded"
184
+ class="tw-rounded tw-self-start"
185
+ :style="{ width: '9rem', height: '3rem' }"
186
+ />
187
+
188
+ <kv-lend-cta
189
+ v-else
190
+ :loan="loan"
191
+ :basket-items="basketItems"
192
+ :is-loading="isLoading"
193
+ :is-adding="isAdding"
194
+ :enable-five-dollars-notes="enableFiveDollarsNotes"
195
+ :kv-track-function="kvTrackFunction"
196
+ class="tw-mt-auto"
197
+ :class="{ 'tw-w-full' : unreservedAmount <= 0 }"
198
+ @add-to-basket="$emit('add-to-basket', $event)"
199
+ />
200
+ </div>
201
+ </div>
202
+ </template>
203
+
204
+ <script>
205
+ import { mdiMapMarker } from '@mdi/js';
206
+ import KvLoanUse from './KvLoanUse.vue';
207
+ import KvBorrowerImage from './KvBorrowerImage.vue';
208
+ import KvLoanProgressGroup from './KvLoanProgressGroup.vue';
209
+ import KvLoanCallouts from './KvLoanCallouts.vue';
210
+ import KvLendCta from './KvLendCta.vue';
211
+ import KvLoanBookmark from './KvLoanBookmark.vue';
212
+ import KvLoanTag from './KvLoanTag.vue';
213
+ import KvMaterialIcon from './KvMaterialIcon.vue';
214
+ import KvLoadingPlaceholder from './KvLoadingPlaceholder.vue';
215
+
216
+ export default {
217
+ name: 'KvClassicLoanCard',
218
+ components: {
219
+ KvBorrowerImage,
220
+ KvLoadingPlaceholder,
221
+ KvLoanUse,
222
+ KvLoanProgressGroup,
223
+ KvMaterialIcon,
224
+ KvLendCta,
225
+ KvLoanTag,
226
+ KvLoanCallouts,
227
+ KvLoanBookmark,
228
+ },
229
+ props: {
230
+ loanId: {
231
+ type: Number,
232
+ required: true,
233
+ },
234
+ loan: {
235
+ type: Object,
236
+ default: null,
237
+ },
238
+ customLoanDetails: {
239
+ type: Boolean,
240
+ default: false,
241
+ },
242
+ useFullWidth: {
243
+ type: Boolean,
244
+ default: false,
245
+ },
246
+ showTags: {
247
+ type: Boolean,
248
+ default: false,
249
+ },
250
+ categoryPageName: {
251
+ type: String,
252
+ default: '',
253
+ },
254
+ enableFiveDollarsNotes: {
255
+ type: Boolean,
256
+ default: false,
257
+ },
258
+ largeCard: {
259
+ type: Boolean,
260
+ default: false,
261
+ },
262
+ isAdding: {
263
+ type: Boolean,
264
+ default: false,
265
+ },
266
+ isVisitor: {
267
+ type: Boolean,
268
+ default: true,
269
+ },
270
+ basketItems: {
271
+ type: Array,
272
+ default: () => ([]),
273
+ },
274
+ isBookmarked: {
275
+ type: Boolean,
276
+ default: false,
277
+ },
278
+ kvTrackFunction: {
279
+ type: Function,
280
+ required: true,
281
+ },
282
+ photoPath: {
283
+ type: String,
284
+ required: true,
285
+ },
286
+ },
287
+ data() {
288
+ return {
289
+ mdiMapMarker,
290
+ };
291
+ },
292
+ computed: {
293
+ isLoading() {
294
+ return !this.loan;
295
+ },
296
+ cardWidth() {
297
+ return this.useFullWidth ? '100%' : '374px';
298
+ },
299
+ borrowerName() {
300
+ return this.loan?.name || '';
301
+ },
302
+ countryName() {
303
+ return this.loan?.geocode?.country?.name || '';
304
+ },
305
+ city() {
306
+ return this.loan?.geocode?.city || '';
307
+ },
308
+ state() {
309
+ return this.loan?.geocode?.state || '';
310
+ },
311
+ distributionModel() {
312
+ return this.loan?.distributionModel || '';
313
+ },
314
+ imageHash() {
315
+ return this.loan?.image?.hash ?? '';
316
+ },
317
+ hasProgressData() {
318
+ // Local resolver values for the progress bar load client-side
319
+ return typeof this.loan?.unreservedAmount !== 'undefined'
320
+ && typeof this.loan?.fundraisingPercent !== 'undefined';
321
+ },
322
+ allDataLoaded() {
323
+ return !this.isLoading && this.hasProgressData;
324
+ },
325
+ fundraisingPercent() {
326
+ return this.loan?.fundraisingPercent ?? 0;
327
+ },
328
+ unreservedAmount() {
329
+ return this.loan?.unreservedAmount ?? '0';
330
+ },
331
+ formattedLocation() {
332
+ if (this.distributionModel === 'direct') {
333
+ const formattedString = `${this.city}, ${this.state}, ${this.countryName}`;
334
+ return formattedString;
335
+ }
336
+ if (this.countryName === 'Puerto Rico') {
337
+ const formattedString = `${this.city}, PR`;
338
+ return formattedString;
339
+ }
340
+ return this.countryName;
341
+ },
342
+ loanUse() {
343
+ return this.loan?.use ?? '';
344
+ },
345
+ loanStatus() {
346
+ return this.loan?.status ?? '';
347
+ },
348
+ loanAmount() {
349
+ return this.loan?.loanAmount ?? '0';
350
+ },
351
+ loanBorrowerCount() {
352
+ return this.loan?.borrowerCount ?? 0;
353
+ },
354
+ imageAspectRatio() {
355
+ if (this.largeCard) {
356
+ return 5 / 8;
357
+ }
358
+ return 3 / 4;
359
+ },
360
+ imageDefaultWidth() {
361
+ return this.largeCard ? 480 : 336;
362
+ },
363
+ imageSizes() {
364
+ if (this.largeCard) {
365
+ return [{ width: 480 }];
366
+ }
367
+ return [
368
+ { width: this.imageDefaultWidth, viewSize: 1024 },
369
+ { width: this.imageDefaultWidth, viewSize: 768 },
370
+ { width: 416, viewSize: 480 },
371
+ { width: 374, viewSize: 414 },
372
+ { width: 335, viewSize: 375 },
373
+ { width: 300 },
374
+ ];
375
+ },
376
+ loanCallouts() {
377
+ const callouts = [];
378
+ const activityName = this.loan?.activity?.name ?? '';
379
+ const sectorName = this.loan?.sector?.name ?? '';
380
+ const tags = this.loan?.tags?.filter((tag) => tag.charAt(0) === '#')
381
+ .map((tag) => tag.substring(1)) ?? [];
382
+ const themes = this.loan?.themes ?? [];
383
+ const categories = {
384
+ ecoFriendly: !!tags
385
+ .filter((t) => t.toUpperCase() === 'ECO-FRIENDLY' || t.toUpperCase() === 'SUSTAINABLE AG').length,
386
+ refugeesIdps: !!themes.filter((t) => t.toUpperCase() === 'REFUGEES/DISPLACED').length,
387
+ singleParents: !!tags.filter((t) => t.toUpperCase() === 'SINGLE PARENT').length,
388
+ };
389
+
390
+ // P1 Category
391
+ // Exp limited to: Eco-friendly, Refugees and IDPs, Single Parents
392
+ if (!this.categoryPageName) {
393
+ if (categories.ecoFriendly) {
394
+ callouts.push('Eco-friendly');
395
+ } else if (categories.refugeesIdps) {
396
+ callouts.push('Refugees and IDPs');
397
+ } else if (categories.singleParents) {
398
+ callouts.push('Single Parent');
399
+ }
400
+ }
401
+
402
+ // P2 Activity
403
+ if (activityName && this.categoryPageName?.toUpperCase() !== activityName.toUpperCase()) {
404
+ callouts.push(activityName);
405
+ }
406
+
407
+ // P3 Sector
408
+ if (sectorName
409
+ && (activityName.toUpperCase() !== sectorName.toUpperCase())
410
+ && (sectorName.toUpperCase() !== this.categoryPageName?.toUpperCase())
411
+ && callouts.length < 2) {
412
+ callouts.push(sectorName);
413
+ }
414
+
415
+ // P4 Tag
416
+ if (!!tags.length && callouts.length < 2) {
417
+ const position = Math.floor(Math.random() * tags.length);
418
+ const tag = tags[position];
419
+ if (!callouts.filter((c) => c.toUpperCase() === tag.toUpperCase()).length) {
420
+ callouts.push(tag);
421
+ }
422
+ }
423
+
424
+ // P5 Theme
425
+ if (!!themes.length && callouts.length < 2) {
426
+ const position = Math.floor(Math.random() * themes.length);
427
+ const theme = themes[position];
428
+ if (!callouts.filter((c) => c.toUpperCase() === theme.toUpperCase()).length) {
429
+ callouts.push(theme);
430
+ }
431
+ }
432
+
433
+ return callouts;
434
+ },
435
+ },
436
+ methods: {
437
+ showLoanDetails(e) {
438
+ if (this.customLoanDetails) {
439
+ e.preventDefault();
440
+ this.$emit('show-loan-details');
441
+ }
442
+ },
443
+ clickReadMore(target) {
444
+ this.kvTrackFunction('Lending', 'click-Read more', target, this.loanId);
445
+ },
446
+ },
447
+ };
448
+ </script>
449
+
450
+ <style lang="postcss" scoped>
451
+ .loan-card-use:hover,
452
+ .loan-card-use:focus {
453
+ @apply tw-text-primary;
454
+ }
455
+ .loan-card-active-hover:hover .loan-card-use {
456
+ @apply tw-underline;
457
+ }
458
+ .loan-card-progress:hover,
459
+ .loan-card-progress:focus {
460
+ @apply tw-no-underline;
461
+ }
462
+ </style>
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <kv-button
3
+ :state="buttonState"
4
+ @click="addToBasket"
5
+ >
6
+ {{ buttonText }}
7
+ </kv-button>
8
+ </template>
9
+
10
+ <script>
11
+ import KvButton from './KvButton.vue';
12
+
13
+ export default {
14
+ name: 'KvLendAmountButton',
15
+ components: {
16
+ KvButton,
17
+ },
18
+ props: {
19
+ loanId: {
20
+ type: Number,
21
+ default: null,
22
+ },
23
+ showNow: {
24
+ type: Boolean,
25
+ default: false,
26
+ },
27
+ amountLeft: {
28
+ type: [Number, String],
29
+ default: 20,
30
+ },
31
+ completeLoan: {
32
+ type: Boolean,
33
+ default: false,
34
+ },
35
+ isAdding: {
36
+ type: Boolean,
37
+ default: false,
38
+ },
39
+ },
40
+ computed: {
41
+ amountValue() {
42
+ return parseFloat(this.amountLeft).toFixed();
43
+ },
44
+ buttonText() {
45
+ let str = '';
46
+
47
+ if (this.completeLoan) {
48
+ str = `Complete loan for $${this.amountValue}`;
49
+ } else {
50
+ str = `Lend $${this.amountValue}`;
51
+ if (this.showNow) {
52
+ str += ' now';
53
+ }
54
+ }
55
+
56
+ return str;
57
+ },
58
+ buttonState() {
59
+ if (this.isAdding) return 'loading';
60
+ return '';
61
+ },
62
+ },
63
+ methods: {
64
+ addToBasket(event) {
65
+ this.$kvTrackEvent(
66
+ 'Lending',
67
+ 'Add to basket (Partial Share)',
68
+ 'lend-button-click',
69
+ this.loanId,
70
+ this.amountLeft,
71
+ );
72
+ this.$emit('add-to-basket', event);
73
+ },
74
+ },
75
+ };
76
+ </script>