Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: my kiva carousel component for borrower status card #5558

Merged
merged 11 commits into from
Oct 3, 2024
Merged
39 changes: 39 additions & 0 deletions .storybook/stories/BorrowerStatusCarousel.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import BorrowerCarousel from '#src/components/MyKiva/BorrowerCarousel.vue';
import { mockLoansArray } from '../utils';
import apolloStoryMixin from "../mixins/apollo-story-mixin";
import cookieStoreStoryMixin from '../mixins/cookie-store-story-mixin';

export default {
title: 'MyKiva/BorrowerCarousel',
component: BorrowerCarousel,
};

const mockLoans = mockLoansArray(3);

const queryResult = {
data: {
lend: {
loan: mockLoans[0]
}
}
};

const story = (args = {}) => {
const template = (_args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { BorrowerCarousel },
mixins: [apolloStoryMixin({ queryResult }), cookieStoreStoryMixin()],
setup() { return { args }; },
template: `
<borrower-carousel v-bind="args" />
`,
});
template.args = args;
return template;
};

export const Default = story({ loans: [mockLoans[0], mockLoans[1], mockLoans[2]] });
export const OneLoan = story({ loans: [mockLoans[0]] });
export const MoreThanLimit = story({ loans: [...mockLoans, mockLoans[0]] });
export const NoActiveLoans = story({ loans: [mockLoans[0], mockLoans[1], mockLoans[2]], hasActiveLoans: false });
export const Empty = story({ loans: [] });
241 changes: 241 additions & 0 deletions src/components/MyKiva/BorrowerCarousel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
<template>
<MyKivaContainer>
<template v-if="isLoading">
<KvLoadingPlaceholder class="tw-my-2 lg:tw-mb-4" :style="{width: '10rem', height: '3rem'}" />
<KvLoadingPlaceholder class="tw-mb-2 lg:tw-mb-3" :style="{width: '17rem', height: '17rem'}" />
</template>
<template v-else>
<h2 v-html="title" class="tw-mb-3.5"></h2>
<div :class="{'tw-flex tw-justify-center': !loans.length }">
<KvButton
v-kv-track-event="[
'portfolio',
'click',
btnEventLabel
]" v-if="!loans.length || !hasActiveLoans"
:to="link"
>
{{ btnCta }}
</KvButton>
</div>
</template>
<div v-if="hasActiveLoans && !isLoading">
<KvTabs @tab-changed="handleChange" v-if="loans.length > 1" class="tabs">
<template #tabNav>
<KvTab v-for="(loan, index) in filteredLoans" :key="index" :label="index + 1" :for-panel="loan.id">
<div class="tw-flex tw-flex-col tw-justify-start tw-items-center tw-w-10">
<div
class="tw-w-8 tw-h-8 tw-mx-auto md:tw-mx-0 tw-border-white tw-border-4
tw-rounded-full tw-shadow"
>
<BorrowerImage
class="tw-w-full tw-rounded-full tw-bg-brand"
:alt="getBorrowerName(loan)"
:aspect-ratio="1"
:default-image="{ width: 80, faceZoom: 50 }"
:hash="getBorrowerHash(loan)"
:images="[
{ width: 80, faceZoom: 50, viewSize: 1024 },
{ width: 72, faceZoom: 50, viewSize: 734 },
{ width: 64, faceZoom: 50 },
]"
/>
</div>
<h5 class="tw-text-center tw-text-ellipsis tw-line-clamp-2 tw-whitespace-normal">
{{ getBorrowerName(loan) }}
</h5>
</div>
</KvTab>
<KvTab v-if="loans.length > 9">
<a
href="/portfolio/loans" v-kv-track-event="[
'portfolio',
'click',
'view-all'
]"
>View all</a>
</KvTab>
</template>
<template #tabPanels>
<KvTabPanel v-for="(loan, index) in loans" :key="index" :id="loan.id">
<p class="tw-hidden" :id="loan.id"></p>
</KvTabPanel>
</template>
</KvTabs>
<div class="carousel-container">
<KvCarousel
ref="carousel"
class="borrower-carousel tw-w-full md:tw-overflow-visible"
:multiple-slides-visible="true"
:slide-max-width="singleSlideWidth"
:embla-options="{ loop: false, align: 'center'}"
>
<template v-for="(loan, index) in loans" #[`slide${index+1}`] :key="loan.id || index">
<BorrowerStatusCard :loan="loan" />
</template>
</KvCarousel>
</div>
<div class="tw-text-right" v-if="hasCompletedBorrowers">
<a
class="tw-text-h5"
href="/portfolio/loans" v-kv-track-event="[
'portfolio',
'click',
'see-all-borrowers'
]"
>See all borrowers</a>
</div>
</div>
</MyKivaContainer>
</template>

<script setup>
import KvTabs from '@kiva/kv-components/vue/KvTabs';
import KvTab from '@kiva/kv-components/vue/KvTab';
import KvTabPanel from '@kiva/kv-components/vue/KvTabPanel';
import KvCarousel from '@kiva/kv-components/vue/KvCarousel';
import KvButton from '@kiva/kv-components/vue/KvButton';
import BorrowerImage from '#src/components/BorrowerProfile/BorrowerImage';
import MyKivaContainer from '#src/components/MyKiva/MyKivaContainer';
import KvLoadingPlaceholder from '@kiva/kv-components/vue/KvLoadingPlaceholder';
import {
defineProps,
ref,
computed,
toRefs,
inject,
onMounted
} from 'vue';
import {
PAYING_BACK,
ENDED,
DEFAULTED,
FUNDED,
FUNDRAISING,
RAISED
} from '#src/api/fixtures/LoanStatusEnum';
import BorrowerStatusCard from './BorrowerStatusCard';

const props = defineProps({
/**
* Array of loans
* */
loans: {
type: Array,
default: () => ([]),
required: true,
},
isLoading: {
type: Boolean,
default: false,
},
});

const $kvTrackEvent = inject('$kvTrackEvent');
const emit = defineEmits(['selected-loan']);

const { loans } = toRefs(props);
const carousel = ref(null);

const hasActiveLoans = computed(() => {
return loans.value.some(loan => [FUNDED, FUNDRAISING, PAYING_BACK, RAISED].includes(loan?.status));
});

const handleChange = event => {
emit('selected-loan', loans.value[event]);
carousel.value.goToSlide(event);
};

const getBorrowerName = loan => {
return loan?.name ?? '';
};

const getBorrowerHash = loan => {
return loan?.image?.hash ?? '';
};

const title = computed(() => {
if (!hasActiveLoans.value) {
return `You changed <u>${loans.value.length} lives</u>!`;
}
if (loans.value.length) {
if (loans.value.length === 1) {
return 'You’re <u>changing a life</u> right now!';
}
return `You’re <u>changing ${loans.value.length} liv</u>es right now!`;
}
return 'Change a life <u>today</u>!';
});

const btnCta = computed(() => {
if (!hasActiveLoans.value) {
return 'See previously supported borrowers';
}
return 'Make a loan';
});

const link = computed(() => {
if (!hasActiveLoans.value) {
return '/portfolio';
}
return '/lend-by-category';
});

const btnEventLabel = computed(() => {
if (!hasActiveLoans.value) {
return 'see-previously-supported-borrowers';
}
return 'Make-a-loan-no-loans-state';
});

const filteredLoans = computed(() => {
return loans.value.slice(0, 9);
});

const singleSlideWidth = computed(() => {
const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 520;

if (viewportWidth < 768) {
return '288px';
} if (window.innerWidth < 1024) {
return '468px';
}
return '520px';
});

const hasCompletedBorrowers = computed(() => {
christian14b marked this conversation as resolved.
Show resolved Hide resolved
return loans.value.some(loan => loan?.status === ENDED || loan?.status === DEFAULTED);
});

onMounted(() => {
if (!hasActiveLoans.value) {
$kvTrackEvent('portfolio', 'view', 'no-active-borrowers');
} else {
$kvTrackEvent('portfolio', 'view', 'active-borrowers', loans.value.length);
}
});

</script>

<style lang="postcss" scoped>

.carousel-container {
max-width: 100%;

@screen md {
max-width: 468px;
}

@screen lg {
max-width: 520px;
}
}

:deep(.borrower-carousel) div.kv-carousel__controls {
@apply tw-hidden;
}

:deep(.tabs) div[role=tablist] {
@apply md:tw-gap-3.5 tw-items-baseline;
}
</style>
26 changes: 15 additions & 11 deletions src/components/MyKiva/BorrowerStatusCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<div class="tw-bg-white tw-absolute tw-top-1 tw-left-1 tw-rounded tw-px-1 tw-py-0.5 tw-font-medium">
🎉 {{ loanStatus }}
</div>
<div class="tw-top-0 tw-h-full tw-w-full tw-overflow-hidden">
<HeroBackground style="height: 96px;" class="!tw-block tw-rounded-t" />
<div class="tw-top-0 tw-h-full tw-w-full tw-overflow-hidden tw-rounded-t">
<HeroBackground style="height: 96px;" class="!tw-block" />
<div class="tw-flex tw-justify-center tw-gap-1.5 tw-flex-col md:tw-flex-row tw-px-1.5 md:tw-px-2.5">
<div class="tw-flex-1">
<div
Expand All @@ -24,7 +24,7 @@
]"
/>
</div>
<h3 class="tw-text-center md:tw-text-left tw-mb-1">
<h3 class="tw-text-center md:tw-text-left tw-mb-1 tw-line-clamp-2">
{{ title }}
</h3>
<p class="tw-text-center md:tw-text-left">
Expand Down Expand Up @@ -81,7 +81,6 @@ import {
import KvExpandable from '#src/components/Kv/KvExpandable';
import LoanNextSteps from '#src/components/Thanks/LoanNextSteps';
import { addMonths, differenceInWeeks } from 'date-fns';
import { isLoanFundraising } from '#src/util/loanUtils';
import KvMaterialIcon from '@kiva/kv-components/vue/KvMaterialIcon';
import {
ref,
Expand All @@ -91,6 +90,9 @@ import {
inject,
onMounted,
} from 'vue';
import {
FUNDRAISING,
} from '#src/api/fixtures/LoanStatusEnum';

const $kvTrackEvent = inject('$kvTrackEvent');

Expand Down Expand Up @@ -124,7 +126,7 @@ const loanFunFact = computed(() => {
switch (borrowerCountry.value) {
case 'United States':

return '3 in 5 U.S. business owners felt less stressed about finances after support from Kiva.**';
return '3 in 5 U.S. business owners felt less stressed about finances after support from Kiva.*';
case 'Puerto Rico':
// eslint-disable-next-line max-len
return 'Small businesses are a crucial part of Puerto Rico\'s economy, employing around 44% of Puerto Rico\'s workforce.';
Expand All @@ -145,13 +147,13 @@ const loanFunFact = computed(() => {
switch (region) {
case 'Central America':
// eslint-disable-next-line max-len
return 'In Central America, 95% of people surveyed said their quality of life improved as a result of their loan.**';
return 'In Central America, 95% of people surveyed said their quality of life improved as a result of their loan.*';
case 'South America':
// eslint-disable-next-line max-len
return 'People living in poverty in South America has decreased from ~30% in 2002 to less than 20% by 2020.';
case 'Africa':
// eslint-disable-next-line max-len
return 'In Africa, 92% of people surveyed said their confidence in their own abilities improved as a result of their loan.**';
return 'In Africa, 92% of people surveyed said their confidence in their own abilities improved as a result of their loan.*';
case 'Middle East':
// eslint-disable-next-line max-len
return 'The number of people with bank accounts is on the rise in the Middle East, a vital step in driving economic opportunity.';
Expand All @@ -160,14 +162,14 @@ const loanFunFact = computed(() => {
return 'Eastern European countries have made progress in reducing poverty levels over the past decade through social protection programs.';
case 'Asia':
// eslint-disable-next-line max-len
return 'In Asia, 86% of people surveyed were better able to manage their finances as a result of their loan.**';
return 'In Asia, 86% of people surveyed were better able to manage their finances as a result of their loan.*';
default:
// eslint-disable-next-line max-len
return 'In areas of Oceania like Fiji, the gender gap is improving—with more women able to access financial services.';
}
});

const isFundraising = computed(() => isLoanFundraising(loan.value));
const isFundraising = computed(() => loan.value?.status === FUNDRAISING);

const loanStatus = computed(() => {
if (isFundraising.value) {
Expand All @@ -188,8 +190,10 @@ const currentStep = computed(() => {
});

const weeksToRepay = computed(() => {
const date = loan.value?.terms?.expectedPayments?.[0]?.dueToKivaDate ?? null;
const today = new Date();
const date = loan.value?.terms?.expectedPayments
?.find(payment => differenceInWeeks(Date.parse(payment?.dueToKivaDate), today) > 0)
?.dueToKivaDate ?? null;
if (date) {
// Get the number of weeks between the first repayment date (in the future) and now
return `${differenceInWeeks(Date.parse(date), today)} weeks`;
Expand All @@ -200,7 +204,7 @@ const weeksToRepay = computed(() => {
const minDate = differenceInWeeks(addMonths(today, 1), today);
const maxDate = differenceInWeeks(addMonths(expDate, 1), today);

if (minDate === maxDate) {
if (minDate === maxDate || maxDate < 0) {
return `${minDate} weeks`;
}

Expand Down
Loading
Loading