@kiva/kv-components 3.69.1 → 3.70.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.70.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.69.1...@kiva/kv-components@3.70.0) (2024-04-01)
7
+
8
+
9
+ ### Features
10
+
11
+ * activity feed added to loan card ([#383](https://github.com/kiva/kv-ui-elements/issues/383)) ([a7a3bfc](https://github.com/kiva/kv-ui-elements/commit/a7a3bfc600742b700eb221fa77d9951d793ca86b))
12
+
13
+
14
+
15
+
16
+
6
17
  ## [3.69.1](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.69.0...@kiva/kv-components@3.69.1) (2024-03-27)
7
18
 
8
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kiva/kv-components",
3
- "version": "3.69.1",
3
+ "version": "3.70.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -75,5 +75,5 @@
75
75
  "optional": true
76
76
  }
77
77
  },
78
- "gitHead": "12d886b4e18e7c961758e6b2c95568d85bed191c"
78
+ "gitHead": "8e428b2a434b924a6a345f133e1a6e80bbede201"
79
79
  }
@@ -41,6 +41,40 @@ const loan = {
41
41
  plannedExpirationDate: nextWeek.toISOString(),
42
42
  };
43
43
 
44
+ const combinedActivities = [
45
+ {
46
+ key: 'Mon Nov 13 2023',
47
+ data: [
48
+ {
49
+ lenderName: 'Erica',
50
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
51
+ text: 'Erica lent $5',
52
+ date: '2023-11-13T10:51:10Z',
53
+ type: 'LendingAction',
54
+ },
55
+ ],
56
+ },
57
+ {
58
+ key: 'Tue Nov 07 2023',
59
+ data: [
60
+ {
61
+ lenderName: 'Joy',
62
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
63
+ text: 'Joy left comment <span class="tw-italic">"I know him and his wife and they work hard to make everything they do the best. His farm and bake goods are amazing. He just keeps working harder and harder to do more and reach out to the community in everyway."</span>',
64
+ date: '2023-11-08T02:37:56Z',
65
+ type: 'Comment',
66
+ },
67
+ {
68
+ lenderName: 'Joy',
69
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
70
+ text: 'Joy lent $25',
71
+ date: '2023-11-08T02:32:20Z',
72
+ type: 'LendingAction',
73
+ },
74
+ ],
75
+ },
76
+ ];
77
+
44
78
  describe('KvClassicLoanCard', () => {
45
79
  it('has no automated accessibility violations', async () => {
46
80
  const { container } = render(KvClassicLoanCard,
@@ -60,6 +94,7 @@ describe('KvClassicLoanCard', () => {
60
94
  const results = await axe(container);
61
95
  expect(results).toHaveNoViolations();
62
96
  });
97
+
63
98
  it('location should be formatted correctly for PR', async () => {
64
99
  const { getByText } = render(KvClassicLoanCard,
65
100
  {
@@ -85,6 +120,7 @@ describe('KvClassicLoanCard', () => {
85
120
  const countryTagElement = getByText('Rincon, PR');
86
121
  expect(countryTagElement).toBeDefined();
87
122
  });
123
+
88
124
  it('location should be formatted correctly for Kiva US', async () => {
89
125
  const { getByText } = render(KvClassicLoanCard,
90
126
  {
@@ -110,6 +146,7 @@ describe('KvClassicLoanCard', () => {
110
146
  const countryTagElement = getByText('Boulder, Colorado, United States');
111
147
  expect(countryTagElement).toBeDefined();
112
148
  });
149
+
113
150
  it('location should be formatted correctly for partner loans', async () => {
114
151
  const { getByText } = render(KvClassicLoanCard,
115
152
  {
@@ -133,4 +170,32 @@ describe('KvClassicLoanCard', () => {
133
170
  const countryTagElement = getByText('Uganda');
134
171
  expect(countryTagElement).toBeDefined();
135
172
  });
173
+
174
+ it('activity feed should be showed correctly', () => {
175
+ const { getByText, getByRole } = render(KvClassicLoanCard,
176
+ {
177
+ props: {
178
+ loanId: loan.id,
179
+ loan: {
180
+ ...loan,
181
+ loanFundraisingInfo: {
182
+ fundedAmount: '950.00',
183
+ isExpiringSoon: false,
184
+ reservedAmount: '0.00',
185
+ },
186
+ lenders: {
187
+ totalCount: 7,
188
+ },
189
+ },
190
+ kvTrackFunction,
191
+ photoPath,
192
+ combinedActivities,
193
+ },
194
+ });
195
+
196
+ const activityText = getByText('7 members lent $950.');
197
+ const activityBtn = getByRole('button', { name: 'See all activity' });
198
+ expect(activityText).toBeDefined();
199
+ expect(activityBtn).toBeDefined();
200
+ });
136
201
  });
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <div class="tw-flex tw-gap-x-1 tw-items-center">
3
+ <div>
4
+ <KvUserAvatar
5
+ :lender-name="activity.lenderName"
6
+ :lender-image-url="activity.lenderImage"
7
+ :is-small="true"
8
+ />
9
+ </div>
10
+ <div>
11
+ <!-- eslint-disable vue/no-v-html -->
12
+ <p v-html="activity.text"></p>
13
+ <!-- eslint-enable -->
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script>
19
+ import KvUserAvatar from './KvUserAvatar.vue';
20
+
21
+ export default {
22
+ name: 'KvActivityRow',
23
+ components: {
24
+ KvUserAvatar,
25
+ },
26
+ props: {
27
+ activity: {
28
+ type: Object,
29
+ required: true,
30
+ },
31
+ },
32
+ };
33
+ </script>
@@ -225,6 +225,29 @@
225
225
  @show-loan-details="clickReadMore('ViewLoan')"
226
226
  />
227
227
  </div>
228
+ <div
229
+ v-if="combinedActivities.length > 0"
230
+ class="tw-pt-1.5"
231
+ >
232
+ <hr class="tw-border-tertiary tw-mb-1 tw-w-5/6 tw-mx-auto">
233
+ <KvLoanActivities
234
+ :loan="loan"
235
+ :combined-activities="combinedActivities"
236
+ :kv-track-function="kvTrackFunction"
237
+ :basket-items="basketItems"
238
+ :is-adding="isAdding"
239
+ :enable-five-dollars-notes="enableFiveDollarsNotes"
240
+ :five-dollars-selected="fiveDollarsSelected"
241
+ :show-view-loan="showViewLoan"
242
+ :custom-loan-details="customLoanDetails"
243
+ :external-links="externalLinks"
244
+ :route="route"
245
+ :user-balance="userBalance"
246
+ :get-cookie="getCookie"
247
+ :set-cookie="setCookie"
248
+ @add-to-basket="$emit('add-to-basket', $event)"
249
+ />
250
+ </div>
228
251
  </div>
229
252
  </template>
230
253
 
@@ -241,6 +264,7 @@ import KvLoanTag from './KvLoanTag.vue';
241
264
  import KvMaterialIcon from './KvMaterialIcon.vue';
242
265
  import KvLoadingPlaceholder from './KvLoadingPlaceholder.vue';
243
266
  import KvLoanTeamPick from './KvLoanTeamPick.vue';
267
+ import KvLoanActivities from './KvLoanActivities.vue';
244
268
 
245
269
  export default {
246
270
  name: 'KvClassicLoanCard',
@@ -255,6 +279,7 @@ export default {
255
279
  KvLoanCallouts,
256
280
  KvLoanBookmark,
257
281
  KvLoanTeamPick,
282
+ KvLoanActivities,
258
283
  },
259
284
  props: {
260
285
  loanId: {
@@ -349,6 +374,10 @@ export default {
349
374
  type: Boolean,
350
375
  default: false,
351
376
  },
377
+ combinedActivities: {
378
+ type: Array,
379
+ default: () => ([]),
380
+ },
352
381
  },
353
382
  setup(props) {
354
383
  const {
@@ -0,0 +1,258 @@
1
+ <template>
2
+ <div
3
+ v-if="combinedActivities.length > 0"
4
+ class="loan-activity"
5
+ >
6
+ <div class="tw-flex tw-justify-center lg:tw-px-0">
7
+ <KvActivityRow
8
+ :activity="singleActivity"
9
+ />
10
+ </div>
11
+ <div class="tw-flex tw-justify-center tw-mt-1 tw-text-small">
12
+ <span v-if="lendersNumber && amountLent">
13
+ {{ lendersNumber }} members lent {{ amountLent }}. &nbsp;
14
+ </span>
15
+ <button
16
+ class="tw-text-action hover:tw-underline"
17
+ @click="showActivityModal"
18
+ >
19
+ See all activity
20
+ </button>
21
+ </div>
22
+ <KvLightbox
23
+ title="Activity Feed Modal"
24
+ :visible="lightboxOpen"
25
+ @lightbox-closed="closeLightbox"
26
+ >
27
+ <template #header>
28
+ <h2 class="tw-text-h3 tw-flex-1 data-hj-suppress">
29
+ {{ modalTitle }}
30
+ </h2>
31
+ </template>
32
+ <div class="tw-overflow-x-hidden tw-px-2 tw-pb-1">
33
+ <div
34
+ v-for="activity in combinedActivities"
35
+ :key="activity.key"
36
+ class="md:tw-px-8 lg:tw-px-14 tw-mt-4"
37
+ >
38
+ <h4 class="tw-text-center tw-text-h4">
39
+ {{ formattedDate(activity.key) }}
40
+ </h4>
41
+ <div class="tw-flex tw-flex-col tw-gap-y-1">
42
+ <KvActivityRow
43
+ v-for="(act, index) in activity.data"
44
+ :key="index"
45
+ :activity="act"
46
+ />
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <div>
51
+ <p
52
+ v-if="errorMsg"
53
+ class="tw-mt-1 tw-text-small tw-text-danger"
54
+ >
55
+ {{ errorMsg }}
56
+ </p>
57
+ <div class="tw-flex tw-justify-end tw-mt-4 tw-pb-2.5 md:tw-pb-4">
58
+ <KvLendCta
59
+ :loan="loan"
60
+ :is-loading="false"
61
+ :kv-track-function="kvTrackFunction"
62
+ :get-cookie="getCookie"
63
+ :set-cookie="setCookie"
64
+ :user-balance="userBalance"
65
+ :basket-items="basketItems"
66
+ :route="route"
67
+ :is-adding="isAdding"
68
+ :enable-five-dollars-notes="enableFiveDollarsNotes"
69
+ @add-to-basket="$emit('add-to-basket', $event)"
70
+ />
71
+ </div>
72
+ </div>
73
+ </KvLightbox>
74
+ </div>
75
+ </template>
76
+
77
+ <script>
78
+ import { computed, ref, toRefs } from 'vue-demi';
79
+ import numeral from 'numeral';
80
+ import { format } from 'date-fns';
81
+ import KvActivityRow from './KvActivityRow.vue';
82
+ import KvLightbox from './KvLightbox.vue';
83
+ import KvLendCta from './KvLendCta.vue';
84
+
85
+ export default {
86
+ name: 'KvLoanActivities',
87
+ components: {
88
+ KvActivityRow,
89
+ KvLightbox,
90
+ KvLendCta,
91
+ },
92
+ props: {
93
+ /**
94
+ * loan object coming from parent component
95
+ */
96
+ loan: {
97
+ type: Object,
98
+ default: () => ({}),
99
+ },
100
+ /**
101
+ * loan combined activities coming from parent component
102
+ */
103
+ combinedActivities: {
104
+ type: Array,
105
+ default: () => ([]),
106
+ },
107
+ /**
108
+ * isAdding flag coming from parent add to basket method
109
+ */
110
+ isAdding: {
111
+ type: Boolean,
112
+ default: false,
113
+ },
114
+ /**
115
+ * basketItems array coming from parent component
116
+ */
117
+ basketItems: {
118
+ type: Array,
119
+ default: () => ([]),
120
+ },
121
+ /**
122
+ * userBalance string coming from parent component
123
+ */
124
+ userBalance: {
125
+ type: String,
126
+ default: undefined,
127
+ },
128
+ /**
129
+ * errorMsg string coming from parent add to basket method
130
+ */
131
+ errorMsg: {
132
+ type: String,
133
+ default: undefined,
134
+ },
135
+ /**
136
+ * kvTrackFunction function to track events from LendCta component
137
+ */
138
+ kvTrackFunction: {
139
+ type: Function,
140
+ required: true,
141
+ },
142
+ /**
143
+ * getCookie function for LendCta component
144
+ */
145
+ getCookie: {
146
+ type: Function,
147
+ default: undefined,
148
+ },
149
+ /**
150
+ * setCookie function for LendCta component
151
+ */
152
+ setCookie: {
153
+ type: Function,
154
+ default: undefined,
155
+ },
156
+ /**
157
+ * route object for LendCta component
158
+ */
159
+ route: {
160
+ type: Object,
161
+ default: undefined,
162
+ },
163
+ enableFiveDollarsNotes: {
164
+ type: Boolean,
165
+ default: false,
166
+ },
167
+ },
168
+ emits: [
169
+ 'add-to-basket',
170
+ ],
171
+ setup(props) {
172
+ const {
173
+ loan,
174
+ combinedActivities,
175
+ kvTrackFunction,
176
+ } = toRefs(props);
177
+
178
+ const lightboxOpen = ref(false);
179
+
180
+ const modalTitle = computed(() => `Activity for ${loan.value?.name}`);
181
+
182
+ const lendersNumber = computed(() => loan.value?.lenders?.totalCount ?? 0);
183
+
184
+ const amountLent = computed(() => {
185
+ const amount = loan.value?.loanFundraisingInfo?.fundedAmount ?? 0;
186
+ return numeral(parseFloat(amount)).format('$0,0');
187
+ });
188
+
189
+ const singleActivity = computed(() => {
190
+ const singleAct = combinedActivities.value.find((activity) => {
191
+ return activity.data.some((element) => {
192
+ return element.type === 'LendingAction';
193
+ });
194
+ });
195
+
196
+ return singleAct?.data[0] ?? {};
197
+ });
198
+
199
+ const formattedDate = (date) => {
200
+ const dateObj = new Date(date);
201
+ return format(dateObj, 'MMM d, yyyy');
202
+ };
203
+ const showActivityModal = () => {
204
+ kvTrackFunction.value('loan-card', 'click', 'see-all-activity');
205
+ lightboxOpen.value = true;
206
+ };
207
+ const closeLightbox = () => {
208
+ lightboxOpen.value = false;
209
+ };
210
+
211
+ return {
212
+ lightboxOpen,
213
+ modalTitle,
214
+ formattedDate,
215
+ showActivityModal,
216
+ closeLightbox,
217
+ singleActivity,
218
+ lendersNumber,
219
+ amountLent,
220
+ };
221
+ },
222
+ };
223
+ </script>
224
+
225
+ <style scoped lang="postcss">
226
+ .loan-activity >>> #kvLightboxBody {
227
+ @apply tw-flex tw-flex-col tw-px-0 tw-pb-0;
228
+ }
229
+
230
+ .loan-activity >>> div > div > div > div > div:first-child {
231
+ box-shadow: var(--kiva-box-shadow);
232
+ }
233
+
234
+ .loan-activity >>> div > div > div > div > div:first-child > div,
235
+ .loan-activity >>> #kvLightboxBody div {
236
+ box-shadow: none;
237
+ }
238
+
239
+ .loan-activity >>> #kvLightboxBody > div:first-child {
240
+ @apply tw-px-4;
241
+ }
242
+
243
+ .loan-activity >>> [role=dialog] {
244
+ min-width: 840px;
245
+ max-width: 840px !important;
246
+
247
+ @media (max-width: calc(840px + 2rem)) {
248
+ min-width: 100%;
249
+ max-width: 100% !important;
250
+ }
251
+ }
252
+
253
+ .loan-activity >>> #kvLightboxBody > div:nth-child(2) {
254
+ @apply tw-px-4;
255
+
256
+ box-shadow: var(--kiva-negative-box-shadow);
257
+ }
258
+ </style>
@@ -0,0 +1,25 @@
1
+ import KvActivityRow from '../KvActivityRow.vue';
2
+
3
+ export default {
4
+ title: 'KvActivityRow',
5
+ component: KvActivityRow,
6
+ };
7
+
8
+ const activity = {
9
+ lenderName: 'Stephanie',
10
+ lenderImage: 'https://www.development.kiva.org/img/s100/26e15431f51b540f31cd9f011cc54f31.jpg',
11
+ text: 'Stephanie lent $25',
12
+ };
13
+
14
+ const story = (args) => {
15
+ const template = (_args, { argTypes }) => ({
16
+ props: Object.keys(argTypes),
17
+ components: { KvActivityRow },
18
+ template: `<KvActivityRow
19
+ :activity="activity" />`,
20
+ });
21
+ template.args = args;
22
+ return template;
23
+ };
24
+
25
+ export const Default = story({ activity });
@@ -30,6 +30,7 @@ const story = (args) => {
30
30
  :show-view-loan="showViewLoan"
31
31
  :custom-callouts="customCallouts"
32
32
  :is-team-pick="isTeamPick"
33
+ :combined-activities="combinedActivities"
33
34
  />
34
35
  </div>
35
36
  `,
@@ -75,6 +76,40 @@ const loan = {
75
76
  plannedExpirationDate: nextWeek.toISOString(),
76
77
  };
77
78
 
79
+ const combinedActivities = [
80
+ {
81
+ key: 'Mon Nov 13 2023',
82
+ data: [
83
+ {
84
+ lenderName: 'Erica',
85
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
86
+ text: 'Erica lent $5',
87
+ date: '2023-11-13T10:51:10Z',
88
+ type: 'LendingAction',
89
+ },
90
+ ],
91
+ },
92
+ {
93
+ key: 'Tue Nov 07 2023',
94
+ data: [
95
+ {
96
+ lenderName: 'Joy',
97
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
98
+ text: 'Joy left comment <span class="tw-italic">"I know him and his wife and they work hard to make everything they do the best. His farm and bake goods are amazing. He just keeps working harder and harder to do more and reach out to the community in everyway."</span>',
99
+ date: '2023-11-08T02:37:56Z',
100
+ type: 'Comment',
101
+ },
102
+ {
103
+ lenderName: 'Joy',
104
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
105
+ text: 'Joy lent $25',
106
+ date: '2023-11-08T02:32:20Z',
107
+ type: 'LendingAction',
108
+ },
109
+ ],
110
+ },
111
+ ];
112
+
78
113
  const kvTrackFunction = () => { };
79
114
 
80
115
  const photoPath = 'https://www-kiva-org.freetls.fastly.net/img/';
@@ -322,3 +357,21 @@ export const TeamPickVisitor = story({
322
357
  isTeamPick: true,
323
358
  isVisitor: true,
324
359
  });
360
+
361
+ export const ActivityFeed = story({
362
+ loanId: loan.id,
363
+ loan: {
364
+ ...loan,
365
+ loanFundraisingInfo: {
366
+ fundedAmount: '950.00',
367
+ isExpiringSoon: false,
368
+ reservedAmount: '0.00',
369
+ },
370
+ lenders: {
371
+ totalCount: 7,
372
+ },
373
+ },
374
+ kvTrackFunction,
375
+ photoPath,
376
+ combinedActivities,
377
+ });
@@ -0,0 +1,104 @@
1
+ import KvLoanActivities from '../KvLoanActivities.vue';
2
+
3
+ export default {
4
+ title: 'KvLoanActivities',
5
+ component: KvLoanActivities,
6
+ };
7
+
8
+ const loan = {
9
+ id: 1998250,
10
+ distributionModel: 'partner', // direct, partner, both
11
+ geocode: {
12
+ city: 'Cranston',
13
+ state: 'RI',
14
+ country: {
15
+ name: 'Malawi',
16
+ isoCode: 'MW',
17
+ },
18
+ },
19
+ image: {
20
+ hash: 'd5ad26cd7acc24317edc1c04c6250074',
21
+ },
22
+ name: 'Microloan Foundation Malawi',
23
+ sector: {
24
+ name: 'Services',
25
+ },
26
+ whySpecial: 'It helps Lending Partners withstand negative economic impacts of the COVID-19 pandemic.',
27
+ userProperties: {
28
+ lentTo: null,
29
+ },
30
+ use: 'this Lending Partner provide loans to women in rural Malawi during the COVID-19 crisis.',
31
+ status: 'fundraising',
32
+ loanAmount: '250000.00',
33
+ borrowerCount: 1,
34
+ anonymizationLevel: 'none',
35
+ fullLoanUse: 'A loan of $250,000 helps this Lending Partner provide loans to women in rural Malawi during the COVID-19 crisis.',
36
+ fundraisingPercent: 0.75,
37
+ unreservedAmount: '600',
38
+ loanFundraisingInfo: {
39
+ fundedAmount: '218950.00',
40
+ reservedAmount: '0.00',
41
+ isExpiringSoon: false,
42
+ },
43
+ lenders: {
44
+ totalCount: 7,
45
+ },
46
+ plannedExpirationDate: '2020-09-10T19:30:13Z',
47
+ matchingText: 'LISC',
48
+ matchRatio: 2,
49
+ };
50
+
51
+ const combinedActivities = [
52
+ {
53
+ key: 'Mon Nov 13 2023',
54
+ data: [
55
+ {
56
+ lenderName: 'Erica',
57
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
58
+ text: 'Erica lent $5',
59
+ date: '2023-11-13T10:51:10Z',
60
+ type: 'LendingAction',
61
+ },
62
+ ],
63
+ },
64
+ {
65
+ key: 'Tue Nov 07 2023',
66
+ data: [
67
+ {
68
+ lenderName: 'Joy',
69
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
70
+ text: 'Joy left comment <span class="tw-italic">"I know him and his wife and they work hard to make everything they do the best. His farm and bake goods are amazing. He just keeps working harder and harder to do more and reach out to the community in everyway."</span>',
71
+ date: '2023-11-08T02:37:56Z',
72
+ type: 'Comment',
73
+ },
74
+ {
75
+ lenderName: 'Joy',
76
+ lenderImage: 'https://www.development.kiva.org/img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg',
77
+ text: 'Joy lent $25',
78
+ date: '2023-11-08T02:32:20Z',
79
+ type: 'LendingAction',
80
+ },
81
+ ],
82
+ },
83
+ ];
84
+
85
+ const kvTrackFunction = () => { };
86
+
87
+ const story = (args) => {
88
+ const template = (_args, { argTypes }) => ({
89
+ props: Object.keys(argTypes),
90
+ components: { KvLoanActivities },
91
+ template: `
92
+ <div style="max-width: 400px;">
93
+ <KvLoanActivities
94
+ :loan="loan"
95
+ :combined-activities="combinedActivities"
96
+ :kv-track-function="kvTrackFunction"
97
+ />
98
+ </div>`,
99
+ });
100
+ template.args = args;
101
+ return template;
102
+ };
103
+
104
+ export const Default = story({ loan, combinedActivities, kvTrackFunction });