@kiva/kv-components 3.106.0 → 3.107.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/.eslintrc.cjs +1 -0
- package/CHANGELOG.md +22 -0
- package/dist/components/.storybook/main.js +85 -0
- package/dist/components/.storybook/package.json +3 -0
- package/dist/components/.storybook/preview.js +61 -0
- package/dist/components/.storybook/tailwind.css +5 -0
- package/dist/components/KvAccordionItem.vue +130 -0
- package/dist/components/KvActivityRow.vue +33 -0
- package/dist/components/KvBorrowerImage.vue +179 -0
- package/dist/components/KvButton.vue +287 -0
- package/dist/components/KvCarousel.vue +297 -0
- package/dist/components/KvCartModal.vue +365 -0
- package/dist/components/KvCheckbox.vue +203 -0
- package/dist/components/KvChip.vue +54 -0
- package/dist/components/KvClassicLoanCard.vue +527 -0
- package/dist/components/KvCommentsAdd.vue +135 -0
- package/dist/components/KvCommentsContainer.vue +84 -0
- package/dist/components/KvCommentsHeartButton.vue +70 -0
- package/dist/components/KvCommentsList.vue +68 -0
- package/dist/components/KvCommentsListItem.vue +241 -0
- package/dist/components/KvCommentsReplyButton.vue +52 -0
- package/dist/components/KvContentfulImg.vue +273 -0
- package/dist/components/KvCountdownTimer.vue +59 -0
- package/dist/components/KvExpandable.vue +84 -0
- package/dist/components/KvExpandableQuestion.vue +120 -0
- package/dist/components/KvFlag.vue +120 -0
- package/dist/components/KvGrid.vue +28 -0
- package/dist/components/KvImpactDashboardHeader.vue +40 -0
- package/dist/components/KvInlineActivityCard.vue +55 -0
- package/dist/components/KvInlineActivityFeed.vue +38 -0
- package/dist/components/KvIntroductionLoanCard.vue +446 -0
- package/dist/components/KvLendAmountButton.vue +65 -0
- package/dist/components/KvLendCta.vue +451 -0
- package/dist/components/KvLightbox.vue +334 -0
- package/dist/components/KvLineGraph.vue +128 -0
- package/dist/components/KvLoadingPlaceholder.vue +38 -0
- package/dist/components/KvLoadingSpinner.vue +81 -0
- package/dist/components/KvLoanActivities.vue +268 -0
- package/dist/components/KvLoanBookmark.vue +39 -0
- package/dist/components/KvLoanCallouts.vue +53 -0
- package/dist/components/KvLoanProgressGroup.vue +76 -0
- package/dist/components/KvLoanTag.vue +88 -0
- package/dist/components/KvLoanTeamPick.vue +44 -0
- package/dist/components/KvLoanUse.vue +92 -0
- package/dist/components/KvMap.vue +599 -0
- package/dist/components/KvMaterialIcon.vue +47 -0
- package/dist/components/KvPageContainer.vue +15 -0
- package/dist/components/KvPagination.vue +198 -0
- package/dist/components/KvPieChart.vue +257 -0
- package/dist/components/KvPopper.vue +178 -0
- package/dist/components/KvProgressBar.vue +149 -0
- package/dist/components/KvRadio.vue +198 -0
- package/dist/components/KvSelect.vue +114 -0
- package/dist/components/KvSideSheet.vue +134 -0
- package/dist/components/KvSwitch.vue +143 -0
- package/dist/components/KvTab.vue +90 -0
- package/dist/components/KvTabPanel.vue +64 -0
- package/dist/components/KvTabs.vue +182 -0
- package/dist/components/KvTextInput.vue +247 -0
- package/dist/components/KvTextLink.vue +138 -0
- package/dist/components/KvThemeProvider.vue +122 -0
- package/dist/components/KvToast.vue +221 -0
- package/dist/components/KvTooltip.vue +168 -0
- package/dist/components/KvTreeMapChart.vue +229 -0
- package/dist/components/KvUserAvatar.vue +132 -0
- package/dist/components/KvVerticalCarousel.vue +156 -0
- package/dist/components/KvVotingCard.vue +160 -0
- package/dist/components/KvVotingCardV2.vue +154 -0
- package/dist/components/KvWideLoanCard.vue +432 -0
- package/dist/components/stories/Forms.stories.js +62 -0
- package/dist/components/stories/KvAccordionItem.stories.js +24 -0
- package/dist/components/stories/KvActivityRow.stories.js +25 -0
- package/dist/components/stories/KvBorrowerImage.stories.js +68 -0
- package/dist/components/stories/KvButton.stories.js +144 -0
- package/dist/components/stories/KvCarousel.stories.js +426 -0
- package/dist/components/stories/KvCartModal.stories.js +54 -0
- package/dist/components/stories/KvCheckbox.stories.js +163 -0
- package/dist/components/stories/KvChip.stories.js +43 -0
- package/dist/components/stories/KvClassicLoanCard.stories.js +480 -0
- package/dist/components/stories/KvCommentsAdd.stories.js +32 -0
- package/dist/components/stories/KvCommentsContainer.stories.js +42 -0
- package/dist/components/stories/KvCommentsHeartButton.stories.js +25 -0
- package/dist/components/stories/KvCommentsList.stories.js +39 -0
- package/dist/components/stories/KvCommentsListItem.stories.js +45 -0
- package/dist/components/stories/KvCommentsReplyButton.stories.js +21 -0
- package/dist/components/stories/KvContentfulImg.stories.js +196 -0
- package/dist/components/stories/KvCountdownTimer.stories.js +30 -0
- package/dist/components/stories/KvExpandableQuestion.stories.js +129 -0
- package/dist/components/stories/KvFlag.stories.js +36 -0
- package/dist/components/stories/KvGrid.stories.js +97 -0
- package/dist/components/stories/KvImpactDashboardHeader.stories.js +22 -0
- package/dist/components/stories/KvInlineActivityCard.stories.js +69 -0
- package/dist/components/stories/KvInlineActivityFeed.stories.js +76 -0
- package/dist/components/stories/KvIntroductionLoanCard.stories.js +208 -0
- package/dist/components/stories/KvLendAmountButton.stories.js +31 -0
- package/dist/components/stories/KvLendCta.stories.js +177 -0
- package/dist/components/stories/KvLightbox.stories.js +304 -0
- package/dist/components/stories/KvLineGraph.stories.js +52 -0
- package/dist/components/stories/KvLoadingPlaceholder.stories.js +17 -0
- package/dist/components/stories/KvLoadingSpinner.stories.js +52 -0
- package/dist/components/stories/KvLoanActivities.stories.js +104 -0
- package/dist/components/stories/KvLoanBookmark.stories.js +22 -0
- package/dist/components/stories/KvLoanCallouts.stories.js +22 -0
- package/dist/components/stories/KvLoanProgressGroup.stories.js +29 -0
- package/dist/components/stories/KvLoanTag.stories.js +61 -0
- package/dist/components/stories/KvLoanTeamPick.stories.js +20 -0
- package/dist/components/stories/KvLoanUse.stories.js +60 -0
- package/dist/components/stories/KvMap.stories.js +121 -0
- package/dist/components/stories/KvMaterialIcon.stories.js +201 -0
- package/dist/components/stories/KvPageContainer.stories.js +50 -0
- package/dist/components/stories/KvPagination.stories.js +70 -0
- package/dist/components/stories/KvPieChart.stories.js +47 -0
- package/dist/components/stories/KvProgressBar.stories.js +53 -0
- package/dist/components/stories/KvRadio.stories.js +140 -0
- package/dist/components/stories/KvSelect.stories.js +125 -0
- package/dist/components/stories/KvSideSheet.stories.js +50 -0
- package/dist/components/stories/KvSwitch.stories.js +66 -0
- package/dist/components/stories/KvTabs.stories.js +106 -0
- package/dist/components/stories/KvTextInput.stories.js +194 -0
- package/dist/components/stories/KvTextLink.stories.js +55 -0
- package/dist/components/stories/KvThemeProvider.stories.js +178 -0
- package/dist/components/stories/KvToast.stories.js +117 -0
- package/dist/components/stories/KvTooltip.stories.js +26 -0
- package/dist/components/stories/KvTreeMapChart.stories.js +42 -0
- package/dist/components/stories/KvUserAvatar.stories.js +47 -0
- package/dist/components/stories/KvVerticalCarousel.stories.js +168 -0
- package/dist/components/stories/KvVotingCard.stories.js +33 -0
- package/dist/components/stories/KvVotingCardV2.stories.js +89 -0
- package/dist/components/stories/KvWideLoanCard.stories.js +292 -0
- package/dist/components/stories/StyleguidePrimitives.stories.js +499 -0
- package/dist/components/stories/StyleguideProse.stories.js +215 -0
- package/dist/data/countries-borders.json +1 -0
- package/dist/data/ne_110m_admin_0_countries.json +1 -0
- package/dist/utils/Alea.cjs +87 -0
- package/dist/utils/Alea.js +9 -0
- package/dist/utils/attrs.cjs +50 -0
- package/dist/utils/attrs.js +7 -0
- package/dist/utils/carousels.cjs +184 -0
- package/dist/utils/carousels.js +8 -0
- package/dist/utils/chunk-3HK4G4NT.js +27 -0
- package/dist/utils/chunk-55HF2ORX.js +201 -0
- package/dist/utils/chunk-AY3PR5S4.js +54 -0
- package/dist/utils/chunk-AZPWOFD5.js +148 -0
- package/dist/utils/chunk-B5J5WLAH.js +18 -0
- package/dist/utils/chunk-GPSH6OPA.js +64 -0
- package/dist/utils/chunk-HIY5IW65.js +28 -0
- package/dist/utils/chunk-HV3AUBFT.js +15 -0
- package/dist/utils/chunk-MSMZIN54.js +110 -0
- package/dist/utils/chunk-OXJCCNNW.js +30 -0
- package/dist/utils/chunk-S3MABILA.js +22 -0
- package/dist/utils/chunk-VIGEMAKO.js +249 -0
- package/dist/utils/chunk-YCNMJ4YV.js +37 -0
- package/dist/utils/chunk-YFEC5ODJ.js +129 -0
- package/dist/utils/expander.cjs +78 -0
- package/dist/utils/expander.js +9 -0
- package/dist/utils/imageUtils.cjs +54 -0
- package/dist/utils/imageUtils.js +9 -0
- package/dist/utils/index.cjs +1118 -0
- package/dist/utils/index.js +166 -0
- package/dist/utils/loanCard.cjs +222 -0
- package/dist/utils/loanCard.js +9 -0
- package/dist/utils/loanUtils.cjs +170 -0
- package/dist/utils/loanUtils.js +23 -0
- package/dist/utils/mapUtils.cjs +276 -0
- package/dist/utils/mapUtils.js +15 -0
- package/dist/utils/printing.cjs +42 -0
- package/dist/utils/printing.js +9 -0
- package/dist/utils/scrollLock.cjs +54 -0
- package/dist/utils/scrollLock.js +13 -0
- package/dist/utils/throttle.cjs +38 -0
- package/dist/utils/throttle.js +7 -0
- package/dist/utils/touchEvents.cjs +47 -0
- package/dist/utils/touchEvents.js +11 -0
- package/dist/utils/treemap.cjs +133 -0
- package/dist/utils/treemap.js +7 -0
- package/package.json +12 -4
- package/utils/index.js +14 -0
- package/vue/KvVerticalCarousel.vue +1 -1
- package/index.js +0 -3
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<figure class="treemap-figure tw-relative">
|
|
3
|
+
<!-- treemap -->
|
|
4
|
+
<div
|
|
5
|
+
v-for="(block, index) in blocks"
|
|
6
|
+
:key="`block-${index}`"
|
|
7
|
+
class="tw-absolute"
|
|
8
|
+
:style="blockPositionStyles(block)"
|
|
9
|
+
>
|
|
10
|
+
<kv-loading-placeholder
|
|
11
|
+
v-if="loading"
|
|
12
|
+
style="width: calc(100% - 0.25rem); height: calc(100% - 0.25rem);"
|
|
13
|
+
/>
|
|
14
|
+
<p
|
|
15
|
+
v-if="!loading"
|
|
16
|
+
class="tw-rounded-sm tw-box-border tw-overflow-hidden tw-text-small"
|
|
17
|
+
:class="colorClasses(index)"
|
|
18
|
+
style="width: calc(100% - 0.25rem); height: calc(100% - 0.25rem);"
|
|
19
|
+
@mouseenter="setHoverBlock(block)"
|
|
20
|
+
>
|
|
21
|
+
<span class="tw-block tw-px-1 tw-py-0.5 tw-truncate">
|
|
22
|
+
{{ block.data.label }} {{ block.data.percent }}
|
|
23
|
+
</span>
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
<!-- tooltip -->
|
|
27
|
+
<div
|
|
28
|
+
ref="tooltipController"
|
|
29
|
+
class="tw-absolute"
|
|
30
|
+
:style="blockPositionStyles(activeBlock)"
|
|
31
|
+
></div>
|
|
32
|
+
<kv-tooltip
|
|
33
|
+
v-if="!loading && tooltipControllerElement"
|
|
34
|
+
:controller="tooltipControllerElement"
|
|
35
|
+
>
|
|
36
|
+
<template #title>
|
|
37
|
+
{{ activeBlock.data.label }}
|
|
38
|
+
</template>
|
|
39
|
+
{{ activeBlock.data.value }} ({{ activeBlock.data.percent }})
|
|
40
|
+
</kv-tooltip>
|
|
41
|
+
</figure>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<script>
|
|
45
|
+
import numeral from 'numeral';
|
|
46
|
+
import kvTokensPrimitives from '@kiva/kv-tokens/primitives.json';
|
|
47
|
+
import { getTreemap } from '../utils/treemap';
|
|
48
|
+
import { throttle } from '../utils/throttle';
|
|
49
|
+
import KvTooltip from './KvTooltip.vue';
|
|
50
|
+
import KvLoadingPlaceholder from './KvLoadingPlaceholder.vue';
|
|
51
|
+
|
|
52
|
+
const { breakpoints } = kvTokensPrimitives;
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
name: 'TreeMapFigure',
|
|
56
|
+
components: {
|
|
57
|
+
KvLoadingPlaceholder,
|
|
58
|
+
KvTooltip,
|
|
59
|
+
},
|
|
60
|
+
props: {
|
|
61
|
+
loading: {
|
|
62
|
+
type: Boolean,
|
|
63
|
+
default: true,
|
|
64
|
+
},
|
|
65
|
+
values: {
|
|
66
|
+
type: Array,
|
|
67
|
+
default: () => [],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
data() {
|
|
71
|
+
return {
|
|
72
|
+
screenWidth: 0,
|
|
73
|
+
activeBlock: { data: {} },
|
|
74
|
+
tooltipControllerElement: null,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
computed: {
|
|
78
|
+
activeBreakpoints() {
|
|
79
|
+
return Object.keys(breakpoints)
|
|
80
|
+
.filter((name) => this.screenWidth >= breakpoints[name]);
|
|
81
|
+
},
|
|
82
|
+
// Used to determine if blocks should be displayed in landscape or portrait orientation
|
|
83
|
+
aboveMedium() {
|
|
84
|
+
return this.activeBreakpoints.includes('md');
|
|
85
|
+
},
|
|
86
|
+
// Calculate the blocks of the treemap, but don't apply any formatting
|
|
87
|
+
rawBlocks() {
|
|
88
|
+
// Use static blocks when no data is available (either loading or no loans)
|
|
89
|
+
if (!this.values?.length) {
|
|
90
|
+
return this.loading ? [
|
|
91
|
+
{
|
|
92
|
+
x: 0, y: 0, width: 60, height: 40,
|
|
93
|
+
},
|
|
94
|
+
{ x: 60, y: 0, height: 40 },
|
|
95
|
+
{ x: 0, y: 40, width: 30 },
|
|
96
|
+
{ x: 30, y: 40 },
|
|
97
|
+
] : [
|
|
98
|
+
{ x: 0, y: 0, data: { label: 'No loans yet' } },
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Calculate treemap blocks using canvas size 100x100 to easily translate to percentages
|
|
103
|
+
const blocks = getTreemap({
|
|
104
|
+
data: this.values,
|
|
105
|
+
width: 100,
|
|
106
|
+
height: 100,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Blocks smaller than 8% in either dimension will not display well,
|
|
110
|
+
// so combine them into one large 'Other' block
|
|
111
|
+
// If we only have 1 small block though, don't make the 'Other' block
|
|
112
|
+
const tooSmall = blocks.filter((block) => block.width <= 8 || block.height <= 8);
|
|
113
|
+
if (tooSmall.length === 1) return blocks;
|
|
114
|
+
|
|
115
|
+
const bigBlocks = blocks.filter((block) => block.width > 8 && block.height > 8);
|
|
116
|
+
return [...bigBlocks, this.reduceBlocks(tooSmall)];
|
|
117
|
+
},
|
|
118
|
+
// This is the array that is iterated over in the template
|
|
119
|
+
blocks() {
|
|
120
|
+
return this.rawBlocks.map((block) => {
|
|
121
|
+
const {
|
|
122
|
+
data, x, y, width, height,
|
|
123
|
+
} = block ?? {};
|
|
124
|
+
const { percent } = data ?? {};
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
data: {
|
|
128
|
+
...data,
|
|
129
|
+
percent: numeral(percent).format('0[.]0%'),
|
|
130
|
+
},
|
|
131
|
+
// flip x/y when going between small (portrait) and medium (landscape) screens
|
|
132
|
+
x: this.aboveMedium ? x : y,
|
|
133
|
+
y: this.aboveMedium ? y : x,
|
|
134
|
+
width: this.aboveMedium ? width : height,
|
|
135
|
+
height: this.aboveMedium ? height : width,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
mounted() {
|
|
141
|
+
this.screenWidth = window.innerWidth;
|
|
142
|
+
window.addEventListener('resize', throttle(() => {
|
|
143
|
+
this.screenWidth = window.innerWidth;
|
|
144
|
+
}, 200));
|
|
145
|
+
|
|
146
|
+
// Used by KvTooltip/KvPopper to position the tooltip
|
|
147
|
+
if (this.$refs?.tooltipController) {
|
|
148
|
+
this.tooltipControllerElement = this.$refs.tooltipController;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
methods: {
|
|
152
|
+
// Used by the p elements in the main v-for loop to determine background and text color
|
|
153
|
+
colorClasses(index) {
|
|
154
|
+
const classes = [];
|
|
155
|
+
const total = this.blocks?.length ?? 1;
|
|
156
|
+
|
|
157
|
+
// background
|
|
158
|
+
const size = (10 - Math.round((index / total) * 9)) * 100;
|
|
159
|
+
classes.push(size === 600 ? 'tw-bg-brand' : `tw-bg-brand-${size}`);
|
|
160
|
+
|
|
161
|
+
// text
|
|
162
|
+
classes.push(size > 700 ? 'tw-text-white' : 'tw-text-black');
|
|
163
|
+
|
|
164
|
+
return classes;
|
|
165
|
+
},
|
|
166
|
+
// Used by the divs in the main v-for for block positions and the tooltip controller div to match positions
|
|
167
|
+
// with the current active block
|
|
168
|
+
blockPositionStyles(block) {
|
|
169
|
+
return {
|
|
170
|
+
left: `${block.x}%`,
|
|
171
|
+
top: `${block.y}%`,
|
|
172
|
+
width: block.width ? `${block.width}%` : null,
|
|
173
|
+
height: block.height ? `${block.height}%` : null,
|
|
174
|
+
right: block.width ? null : '0',
|
|
175
|
+
bottom: block.height ? null : '0',
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
// Combine all small blocks into a single 'Other' block that will be at the bottom right of the figure.
|
|
179
|
+
reduceBlocks(blocks) {
|
|
180
|
+
return blocks?.reduce((other, block) => {
|
|
181
|
+
/* eslint-disable no-param-reassign */
|
|
182
|
+
// Use the smallest x for the overall x
|
|
183
|
+
if (block.x < other.x) {
|
|
184
|
+
other.x = block.x;
|
|
185
|
+
}
|
|
186
|
+
// Use the smallest y for the overall y
|
|
187
|
+
if (block.y < other.y) {
|
|
188
|
+
other.y = block.y;
|
|
189
|
+
}
|
|
190
|
+
// Sum up all values and percents
|
|
191
|
+
other.data.value += block.data.value;
|
|
192
|
+
other.data.percent += block.data.percent;
|
|
193
|
+
return other;
|
|
194
|
+
/* eslint-ensable no-param-reassign */
|
|
195
|
+
}, {
|
|
196
|
+
x: 100,
|
|
197
|
+
y: 100,
|
|
198
|
+
width: 0,
|
|
199
|
+
height: 0,
|
|
200
|
+
data: {
|
|
201
|
+
label: 'Other',
|
|
202
|
+
value: 0,
|
|
203
|
+
percent: 0,
|
|
204
|
+
},
|
|
205
|
+
}) ?? [];
|
|
206
|
+
},
|
|
207
|
+
// Sets the block that will be used to position the tooltip and change the info displayed in the tooltip
|
|
208
|
+
setHoverBlock(block) {
|
|
209
|
+
this.activeBlock = block;
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
</script>
|
|
214
|
+
|
|
215
|
+
<style lang="postcss" scoped>
|
|
216
|
+
.treemap-figure {
|
|
217
|
+
height: 30rem;
|
|
218
|
+
|
|
219
|
+
/* account for every block having a 0.25rem right margin */
|
|
220
|
+
width: calc(100% + 0.25rem);
|
|
221
|
+
margin-right: -0.25rem;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@screen md {
|
|
225
|
+
.treemap-figure {
|
|
226
|
+
height: 20rem;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="data-hj-suppress tw-flex"
|
|
4
|
+
:class="{ 'tw-w-3': isSmall, 'tw-w-6': !isSmall }"
|
|
5
|
+
>
|
|
6
|
+
<!-- User is anonymous or data is missing -->
|
|
7
|
+
<div
|
|
8
|
+
v-if="isAnonymousUser"
|
|
9
|
+
class="
|
|
10
|
+
tw-rounded-full
|
|
11
|
+
tw-bg-brand
|
|
12
|
+
tw-inline-flex tw-align-center tw-justify-center
|
|
13
|
+
"
|
|
14
|
+
:class="{ 'tw-w-3 tw-h-3': isSmall, 'tw-w-6 tw-h-6': !isSmall }"
|
|
15
|
+
>
|
|
16
|
+
<!-- Kiva K logo -->
|
|
17
|
+
<!-- eslint-disable max-len -->
|
|
18
|
+
<svg
|
|
19
|
+
class="tw-h-full tw-text-brand"
|
|
20
|
+
:class="{ 'tw-w-3 tw-h-3': isSmall }"
|
|
21
|
+
width="25"
|
|
22
|
+
height="37"
|
|
23
|
+
viewBox="0 0 25 37"
|
|
24
|
+
fill="none"
|
|
25
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
26
|
+
>
|
|
27
|
+
<path
|
|
28
|
+
d="M8.22861 0.875H0.857178V36.3125H8.22861V0.875Z"
|
|
29
|
+
fill="white"
|
|
30
|
+
/>
|
|
31
|
+
<path
|
|
32
|
+
d="M10.1143 23.2751C21.9428 23.2751 24.6857 13.2126 24.6857 11.4626H23.6571C11.8286 11.4626 9.08569 21.5251 9.08569 23.2751H10.1143Z"
|
|
33
|
+
fill="white"
|
|
34
|
+
/>
|
|
35
|
+
<path
|
|
36
|
+
d="M9.08569 24.2376C9.08569 26.0751 11.1428 36.3126 23.8285 36.3126H24.8571C24.8571 34.4751 22.8 24.2376 10.1143 24.2376H9.08569Z"
|
|
37
|
+
fill="white"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
<!-- eslint-enable max-len -->
|
|
41
|
+
</div>
|
|
42
|
+
<!-- User is not anonymous and has an image -->
|
|
43
|
+
<div
|
|
44
|
+
v-else-if="!isLegacyPlaceholderAvatar(imageFilename) && imageFilename"
|
|
45
|
+
>
|
|
46
|
+
<img
|
|
47
|
+
:src="lenderImageUrl"
|
|
48
|
+
alt="Image of lender"
|
|
49
|
+
class="tw-rounded-full tw-inline-block"
|
|
50
|
+
:class="{ 'tw-w-3 tw-h-3': isSmall, 'tw-w-6 tw-h-6': !isSmall }"
|
|
51
|
+
>
|
|
52
|
+
</div>
|
|
53
|
+
<!-- User is not anonymous and does not have an image -->
|
|
54
|
+
<div
|
|
55
|
+
v-else-if="isLegacyPlaceholderAvatar(imageFilename) || !imageFilename"
|
|
56
|
+
class="
|
|
57
|
+
tw-rounded-full
|
|
58
|
+
tw-inline-flex tw-align-center tw-justify-center
|
|
59
|
+
"
|
|
60
|
+
:class="avatarClass()"
|
|
61
|
+
>
|
|
62
|
+
<!-- First Letter of lender name -->
|
|
63
|
+
<span class="tw-self-center">
|
|
64
|
+
{{ lenderNameFirstLetter }}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script>
|
|
71
|
+
import { computed, toRefs } from 'vue-demi';
|
|
72
|
+
import { isLegacyPlaceholderAvatar, randomizedUserAvatarClass } from '../utils/imageUtils';
|
|
73
|
+
|
|
74
|
+
export default {
|
|
75
|
+
name: 'KvUserAvatar',
|
|
76
|
+
props: {
|
|
77
|
+
/**
|
|
78
|
+
* The name of the lender
|
|
79
|
+
*/
|
|
80
|
+
lenderName: {
|
|
81
|
+
type: String,
|
|
82
|
+
default: '',
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* The image of the lender
|
|
86
|
+
*/
|
|
87
|
+
lenderImageUrl: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: '',
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* The image of the lender
|
|
93
|
+
*/
|
|
94
|
+
isSmall: {
|
|
95
|
+
type: Boolean,
|
|
96
|
+
default: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
setup(props) {
|
|
100
|
+
const {
|
|
101
|
+
lenderName,
|
|
102
|
+
lenderImageUrl,
|
|
103
|
+
isSmall,
|
|
104
|
+
} = toRefs(props);
|
|
105
|
+
|
|
106
|
+
const isAnonymousUser = computed(() => {
|
|
107
|
+
return (lenderName.value === '' && lenderImageUrl.value === '') || lenderName.value === 'Anonymous';
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const avatarClass = () => {
|
|
111
|
+
const smallClass = isSmall?.value ? 'tw-w-3 tw-h-3 tw-text-h4' : 'tw-w-6 tw-h-6 tw-text-h2';
|
|
112
|
+
return `${randomizedUserAvatarClass()} ${smallClass}`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const imageFilename = computed(() => {
|
|
116
|
+
return lenderImageUrl?.value?.split('/').pop() ?? '';
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const lenderNameFirstLetter = computed(() => {
|
|
120
|
+
return lenderName?.value?.substring(0, 1).toUpperCase();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
isAnonymousUser,
|
|
125
|
+
avatarClass,
|
|
126
|
+
imageFilename,
|
|
127
|
+
lenderNameFirstLetter,
|
|
128
|
+
isLegacyPlaceholderAvatar,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
</script>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<section
|
|
4
|
+
ref="rootEl"
|
|
5
|
+
aria-label="carousel"
|
|
6
|
+
class="kv-carousel tw-overflow-hidden tw-relative"
|
|
7
|
+
>
|
|
8
|
+
<!-- Carousel Content -->
|
|
9
|
+
<div
|
|
10
|
+
class="tw-flex tw-flex-col"
|
|
11
|
+
:style="`height: ${heightStyle}`"
|
|
12
|
+
@click.capture="onCarouselContainerClick"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
v-for="(slotName, index) in componentSlotKeys"
|
|
16
|
+
:key="index"
|
|
17
|
+
role="group"
|
|
18
|
+
:aria-label="`slide ${index + 1} of ${componentSlotKeys.length}`"
|
|
19
|
+
:aria-current="currentIndex === index ? 'true' : 'false'"
|
|
20
|
+
:aria-hidden="isAriaHidden(index)? 'true' : 'false'"
|
|
21
|
+
:tab-index="isAriaHidden(index) ? '-1' : false"
|
|
22
|
+
>
|
|
23
|
+
<slot
|
|
24
|
+
:name="slotName"
|
|
25
|
+
></slot>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
<!-- Carousel Controls -->
|
|
30
|
+
<div
|
|
31
|
+
class="kv-carousel__controls tw-flex
|
|
32
|
+
tw-justify-between md:tw-justify-center tw-items-center tw-gap-2
|
|
33
|
+
tw-mt-2 tw-w-full"
|
|
34
|
+
>
|
|
35
|
+
<button
|
|
36
|
+
class="tw-text-primary
|
|
37
|
+
tw-rounded-full
|
|
38
|
+
tw-border-2 tw-border-primary
|
|
39
|
+
tw-h-4 tw-w-4
|
|
40
|
+
tw-flex tw-items-center tw-justify-center
|
|
41
|
+
disabled:tw-opacity-low disabled:tw-cursor-default"
|
|
42
|
+
:disabled="embla && !embla.canScrollPrev()"
|
|
43
|
+
@click="handleUserInteraction(previousIndex, 'click-left-arrow')"
|
|
44
|
+
>
|
|
45
|
+
<kv-material-icon
|
|
46
|
+
class="tw-w-4"
|
|
47
|
+
:icon="mdiChevronUp"
|
|
48
|
+
/>
|
|
49
|
+
<span class="tw-sr-only">Show previous slide</span>
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
class="tw-text-primary
|
|
53
|
+
tw-rounded-full
|
|
54
|
+
tw-border-2 tw-border-primary
|
|
55
|
+
tw-h-4 tw-w-4
|
|
56
|
+
tw-flex tw-items-center tw-justify-center
|
|
57
|
+
disabled:tw-opacity-low disabled:tw-cursor-default"
|
|
58
|
+
:disabled="embla && !embla.canScrollNext()"
|
|
59
|
+
@click="handleUserInteraction(nextIndex, 'click-right-arrow')"
|
|
60
|
+
>
|
|
61
|
+
<kv-material-icon
|
|
62
|
+
class="tw-w-4"
|
|
63
|
+
:icon="mdiChevronDown"
|
|
64
|
+
/>
|
|
65
|
+
<span class="tw-sr-only">Show next slide</span>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
import {
|
|
73
|
+
mdiChevronUp,
|
|
74
|
+
mdiChevronDown,
|
|
75
|
+
} from '@mdi/js';
|
|
76
|
+
import { carouselUtil } from '../utils/carousels';
|
|
77
|
+
import KvMaterialIcon from './KvMaterialIcon.vue';
|
|
78
|
+
|
|
79
|
+
export default {
|
|
80
|
+
components: {
|
|
81
|
+
KvMaterialIcon,
|
|
82
|
+
},
|
|
83
|
+
props: {
|
|
84
|
+
/**
|
|
85
|
+
* Height style declaration of the vertical carousel.
|
|
86
|
+
* */
|
|
87
|
+
heightStyle: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: '400px;',
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* Options for the embla carousel - // https://davidcetinkaya.github.io/embla-carousel/api#options
|
|
93
|
+
* */
|
|
94
|
+
emblaOptions: {
|
|
95
|
+
type: Object,
|
|
96
|
+
default() {
|
|
97
|
+
return {};
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* The type of logic to implement when deciding how many slides
|
|
102
|
+
* to scroll when pressing the next/prev button
|
|
103
|
+
* `visible, auto`
|
|
104
|
+
* */
|
|
105
|
+
slidesToScroll: {
|
|
106
|
+
type: String,
|
|
107
|
+
default: 'auto',
|
|
108
|
+
validator: (value) => ['visible', 'auto'].indexOf(value) !== -1,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
emits: [
|
|
112
|
+
'change',
|
|
113
|
+
'interact-carousel',
|
|
114
|
+
],
|
|
115
|
+
setup(props, { emit, slots }) {
|
|
116
|
+
const {
|
|
117
|
+
componentSlotKeys,
|
|
118
|
+
currentIndex,
|
|
119
|
+
embla,
|
|
120
|
+
goToSlide,
|
|
121
|
+
handleUserInteraction,
|
|
122
|
+
isAriaHidden,
|
|
123
|
+
nextIndex,
|
|
124
|
+
onCarouselContainerClick,
|
|
125
|
+
previousIndex,
|
|
126
|
+
reInit,
|
|
127
|
+
reInitVisible,
|
|
128
|
+
rootEl,
|
|
129
|
+
slideIndicatorCount,
|
|
130
|
+
slides,
|
|
131
|
+
} = carouselUtil(props, { emit, slots }, { axis: 'y' });
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
componentSlotKeys,
|
|
135
|
+
currentIndex,
|
|
136
|
+
embla,
|
|
137
|
+
goToSlide,
|
|
138
|
+
handleUserInteraction,
|
|
139
|
+
isAriaHidden,
|
|
140
|
+
mdiChevronDown,
|
|
141
|
+
mdiChevronUp,
|
|
142
|
+
nextIndex,
|
|
143
|
+
onCarouselContainerClick,
|
|
144
|
+
previousIndex,
|
|
145
|
+
reInit,
|
|
146
|
+
reInitVisible,
|
|
147
|
+
rootEl,
|
|
148
|
+
slideIndicatorCount,
|
|
149
|
+
slides,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
<style scoped>
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="
|
|
4
|
+
kv-voting-card
|
|
5
|
+
tw-bg-white
|
|
6
|
+
tw-rounded
|
|
7
|
+
tw-shadow
|
|
8
|
+
tw-p-1
|
|
9
|
+
tw-flex
|
|
10
|
+
tw-flex-col
|
|
11
|
+
tw-max-w-300"
|
|
12
|
+
>
|
|
13
|
+
<div class="tw-relative tw-flex tw-w-full tw-bg-black tw-rounded tw-mb-1">
|
|
14
|
+
<slot name="image"></slot>
|
|
15
|
+
<div
|
|
16
|
+
class="
|
|
17
|
+
tw-absolute
|
|
18
|
+
tw-bottom-1
|
|
19
|
+
tw-left-1
|
|
20
|
+
tw-text-primary
|
|
21
|
+
tw-bg-white
|
|
22
|
+
tw-rounded
|
|
23
|
+
tw-text-h4
|
|
24
|
+
tw-inline-flex
|
|
25
|
+
tw-items-center"
|
|
26
|
+
style="padding: 2px 6px; text-transform: capitalize;"
|
|
27
|
+
>
|
|
28
|
+
<span
|
|
29
|
+
class="tw-inline-flex tw-align-text-top"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
role="img"
|
|
32
|
+
>
|
|
33
|
+
<svg
|
|
34
|
+
class="tw-h-2 tw-w-2.5"
|
|
35
|
+
viewBox="0 0 20 20"
|
|
36
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
37
|
+
>
|
|
38
|
+
<!-- eslint-disable max-len -->
|
|
39
|
+
<path
|
|
40
|
+
d="m3.12088 18.7441c12.86212 0 15.87912-14.52631 15.87912-16.99996h-1.1209c-12.85272 0-15.8791 14.51376-15.8791 16.99996z"
|
|
41
|
+
fill="currentColor"
|
|
42
|
+
/>
|
|
43
|
+
<!-- eslint-enable max-len -->
|
|
44
|
+
</svg>
|
|
45
|
+
</span>
|
|
46
|
+
<div>
|
|
47
|
+
{{ borrowerName }}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="tw-flex tw-flex-grow">
|
|
52
|
+
<div class="tw-flex-grow">
|
|
53
|
+
<h3 class="tw-font-medium">
|
|
54
|
+
{{ category }}
|
|
55
|
+
</h3>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="tw-flex-shrink">
|
|
58
|
+
<kv-material-icon
|
|
59
|
+
v-if="showInfoIcon"
|
|
60
|
+
:icon="infoIcon"
|
|
61
|
+
class="tw-h-3 tw-w-3"
|
|
62
|
+
@click="handleInfoClick"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="tw-flex tw-items-center tw-w-full tw-mb-1">
|
|
67
|
+
<kv-progress-bar
|
|
68
|
+
v-if="showPercentage"
|
|
69
|
+
class="tw-flex-grow"
|
|
70
|
+
:aria-label="'Percent of votes for ' + category"
|
|
71
|
+
:value="percentage"
|
|
72
|
+
/>
|
|
73
|
+
<div
|
|
74
|
+
v-if="showPercentage"
|
|
75
|
+
class="tw-ml-2 tw-font-medium"
|
|
76
|
+
>
|
|
77
|
+
{{ percentage }}%
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<kv-button
|
|
81
|
+
v-if="showVoteButton"
|
|
82
|
+
class="tw-w-full"
|
|
83
|
+
@click="castVote"
|
|
84
|
+
>
|
|
85
|
+
Vote
|
|
86
|
+
</kv-button>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<script>
|
|
91
|
+
import { mdiInformation } from '@mdi/js';
|
|
92
|
+
import { computed } from 'vue-demi';
|
|
93
|
+
import KvProgressBar from './KvProgressBar.vue';
|
|
94
|
+
import KvButton from './KvButton.vue';
|
|
95
|
+
import KvMaterialIcon from './KvMaterialIcon.vue';
|
|
96
|
+
|
|
97
|
+
export default {
|
|
98
|
+
name: 'KvVotingCard',
|
|
99
|
+
components: {
|
|
100
|
+
KvProgressBar,
|
|
101
|
+
KvButton,
|
|
102
|
+
KvMaterialIcon,
|
|
103
|
+
},
|
|
104
|
+
props: {
|
|
105
|
+
borrowerName: {
|
|
106
|
+
type: String,
|
|
107
|
+
default: '',
|
|
108
|
+
},
|
|
109
|
+
category: {
|
|
110
|
+
type: String,
|
|
111
|
+
default: '',
|
|
112
|
+
},
|
|
113
|
+
images: {
|
|
114
|
+
type: Array,
|
|
115
|
+
default: () => [],
|
|
116
|
+
},
|
|
117
|
+
percentage: {
|
|
118
|
+
type: Number,
|
|
119
|
+
default: 0,
|
|
120
|
+
},
|
|
121
|
+
showVoteButton: {
|
|
122
|
+
type: Boolean,
|
|
123
|
+
default: true,
|
|
124
|
+
},
|
|
125
|
+
showPercentage: {
|
|
126
|
+
type: Boolean,
|
|
127
|
+
default: true,
|
|
128
|
+
},
|
|
129
|
+
showInfoIcon: {
|
|
130
|
+
type: Boolean,
|
|
131
|
+
default: false,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
setup() {
|
|
135
|
+
const infoIcon = computed(() => mdiInformation);
|
|
136
|
+
return {
|
|
137
|
+
infoIcon,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
methods: {
|
|
142
|
+
castVote() {
|
|
143
|
+
this.$emit('vote', {
|
|
144
|
+
category: this.category,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
handleInfoClick() {
|
|
148
|
+
this.$emit('info-click', {
|
|
149
|
+
category: this.category,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
<style scoped>
|
|
157
|
+
.kv-voting-card {
|
|
158
|
+
max-width: 300px;
|
|
159
|
+
}
|
|
160
|
+
</style>
|