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: work in progress - initial UI for Loans Beta Page – Backend integration pending #5781

Merged
merged 25 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fab37d2
feat: new page for portfolio loans started
michelleinez Jan 17, 2025
adddac5
feat: graphql related additions, getting data into lending stats
michelleinez Jan 17, 2025
bf7a651
fix: lint errors
michelleinez Jan 17, 2025
fba6be9
feat: updated table styles and routes
michelleinez Feb 6, 2025
9e279cb
fix: linting errors
michelleinez Feb 6, 2025
ff3cd6f
fix: linting errors
michelleinez Feb 6, 2025
e01f73a
fix: lint issue
michelleinez Feb 7, 2025
8cd3ca1
feat: changed source of graphql data location
michelleinez Feb 7, 2025
a15ae56
fix: amount repaid and lent fixed
michelleinez Feb 7, 2025
7244dbc
feat: stylistic changes to table
michelleinez Feb 11, 2025
c385d68
feat: added table loaders
michelleinez Feb 11, 2025
471da90
fix: update import statements to match new syntax
michelleinez Feb 11, 2025
edd68d2
fix: code cleanup, urls
michelleinez Feb 12, 2025
8a14bcc
fix: table styling
michelleinez Feb 13, 2025
16a8fde
fix: remove duplicate graphql file
michelleinez Feb 13, 2025
e8a667b
fix: clean up loan count rows
michelleinez Feb 13, 2025
93260a2
fix: table abstraction for dynamic table population
michelleinez Feb 13, 2025
183ca78
fix: change loan stats table to table elements
michelleinez Feb 13, 2025
9051022
fix: reverting
michelleinez Feb 13, 2025
7dccfe7
fix: convert stats table to table element
michelleinez Feb 13, 2025
c104f46
feat: add aria table and row roles to grid
michelleinez Feb 13, 2025
fdc913b
fix: removed line
michelleinez Feb 13, 2025
a7b142a
fix: addressing pr comments
michelleinez Feb 13, 2025
fbdada1
fix: logformatter
michelleinez Feb 13, 2025
510dec4
fix: restored graphql file and created new query
michelleinez Feb 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/components/Portfolio/LoanFilterBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div>
<div class="tw-flex tw-flex-col">
<div class="tw-flex tw-justify-between tw-mb-2">
<div class="tw-text-sm tw-text-h2">
Filter loans
</div>
<kv-button
class="tw-text-sm"
variant="primary"
>
Export {{ totalLoans }} loans
</kv-button>
</div>
<kv-text-input
id="loan-filter-text-input"
v-model="searchText"
placeholder="Search by name, ID, partner or location"
/>
<div class="tw-flex tw-flex-wrap tw-items-center tw-gap-2 tw-mt-2">
<div class="tw-flex tw-items-center tw-gap-2">
<span class="tw-text-secondary">Status:</span>
<kv-select
id="loan-filter-select"
v-model="selectedStatus"
>
<option value="all">
All statuses
</option>
</kv-select>
</div>
<div class="tw-flex tw-items-center tw-gap-2">
<span class="tw-text-secondary">Filter by:</span>
<kv-select
v-model="selectedLocation"
>
<option value="all">
All regions
</option>
</kv-select>
<kv-select
v-model="selectedPartner"
>
<option value="all">
All partners
</option>
</kv-select>
</div>
</div>
</div>
<div class="tw-text-secondary tw-mt-2">
Sorted by date posted on Kiva
</div>
</div>
</template>

<script setup>
import { ref } from 'vue';
import { KvSelect, KvTextInput, KvButton } from '@kiva/kv-components';

defineProps({
totalLoans: {
type: Number,
default: 0
}
});

const searchText = ref('');
const selectedStatus = ref('all');
const selectedLocation = ref('all');
const selectedPartner = ref('all');
</script>
251 changes: 251 additions & 0 deletions src/components/Portfolio/LoanList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<template>
<div class="tw-mt-4">
<div class="tw-relative">
<div class="tw-overflow-x-auto tw-min-w-full">
<table class="tw-w-full">
<thead>
<tr class="tw-border-y tw-border-tertiary">
<th class="tw-text-left tw-font-medium tw-px-2 tw-py-1">
Loan details
</th>
<th class="tw-text-left tw-font-medium tw-px-2 tw-py-1">
Status
</th>
<th class="tw-text-right tw-font-medium tw-px-2 tw-py-1">
You loaned
</th>
<th class="tw-text-right tw-font-medium tw-px-2 tw-py-1">
Amount
</th>
<th class="tw-text-right tw-font-medium tw-px-2 tw-py-1">
Length
</th>
<th class="tw-text-right tw-font-medium tw-px-2 tw-py-1">
Paid back or raised
</th>
<th class="tw-text-left tw-font-medium tw-px-2 tw-py-1">
Team
</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="7" class="tw-px-2 tw-py-4">
<div v-for="i in 3" :key="i" class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4">
<kv-loading-placeholder
v-for="(placeholder, index) in placeholders"
:key="index"
:class="[
`tw-col-span-${placeholder.span}`,
placeholder.marginLeft && 'tw-ml-auto'
]"
style="height: 16px;"
/>
</div>
</td>
</tr>
<tr v-else-if="!loans.length">
<td class="tw-text-center tw-text-secondary tw-px-2" colspan="7">
No loans found
</td>
</tr>
<tr
v-for="loan in loans"
:key="loan.id"
class="tw-border-b tw-border-tertiary"
>
<td class="tw-px-2 tw-py-2">
<div class="tw-flex tw-items-center">
<img
:src="loan.image.url"
alt="Loan image"
class="loan-image tw-mr-2"
>
<div>
<div class="tw-font-semibold">
<a
:href="`/lend/${loan.id}`"
class="tw-text-action"
v-kv-track-event="[
'portfolio', 'click', 'View borrower details', loan.name, loan.id]"
>
{{ loan.name }}
<div>
#{{ loan.id }}
</div>
</a>
</div>
<div class="tw-text-secondary">
{{ loan.sector?.name || '-' }}
</div>
<div class="tw-flex tw-items-center tw-text-secondary">
<div class="tw-w-2 tw-h-2 tw-mr-1">
<kv-flag
v-if="loan.geocode?.country?.isoCode"
:country="loan.geocode?.country?.isoCode"
/>
</div>
{{ loan.geocode?.country?.name || '-' }}
</div>
<div class="tw-text-secondary" v-if="loan.partnerName">
<a
:href="getPartnerUrl(loan.partnerId)"
target="_blank"
>
{{ loan.partnerName }}
</a>
</div>
</div>
</div>
</td>
<td class="tw-px-2">
<div class="tw-text-secondary">
{{ getPrintableStatus(loan.status) }}
</div>
</td>
<td class="tw-text-right tw-px-2">
<div>
<div class="tw-mb-1">
${{ loan.userProperties.loanBalance.amountPurchasedByLender }}
</div>
<div class="tw-mb-1 tw-text-secondary">
{{ formatDate(loan.terms.disbursalDate) || '(Endpoint TBD)' }}
</div>
</div>
</td>
<td class="tw-text-right tw-px-2">
<div>
<div>
${{ loan.terms.loanAmount }}
</div>
</div>
</td>
<td class="tw-text-right tw-px-2">
<div>
{{ loan.lenderRepaymentTerm || '-' }} mos
</div>
</td>
<td class="tw-text-right tw-px-2">
<div>
<div class="tw-mb-1">
${{ loan.userProperties.loanBalance.amountRepaidToLender }} repaid to you
</div>
</div>
</td>
<td class="team-cell tw-whitespace-normal tw-break-words tw-px-2">
<div class="tw-items-center">
<template v-if="loan.teams?.values?.[0]">
<img
v-if="loan.teams.values[0].image?.url"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to leave a comment here noting that this is still pending a backend decision on exactly where we will get the team data... Using the [0]th element here is not the intended solution, but it does allow the component to display a dummy team for the time being.

:src="loan.teams.values[0].image.url"
:alt="`${loan.teams.values[0].name} team image`"
class="tw-w-5 tw-h-5"
>
<span>{{ loan.teams.values[0].name }}</span>
</template>
<span v-else>-</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="scroll-gradient"></div>
</div>
</div>
</template>

<script>
import { KvFlag, KvLoadingPlaceholder } from '@kiva/kv-components';
import {
DEFAULTED,
ENDED,
EXPIRED,
FUNDED,
FUNDRAISING,
PAYING_BACK,
RAISED,
REFUNDED,
} from '#src/api/fixtures/LoanStatusEnum';

const DELINQUENT = 'payingBackDelinquent';

export default {
name: 'LoanList',
inject: ['cookieStore'],
props: {
loans: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: true
}
},
components: {
KvFlag,
KvLoadingPlaceholder
},
methods: {
formatDate(date) {
if (!date) return '';
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
},
getPrintableStatus(rawStatus) {
const mapping = {
[FUNDRAISING]: 'Fundraising',
[FUNDED]: 'Funded',
[RAISED]: 'Raised',
[PAYING_BACK]: 'Paying Back',
[ENDED]: 'Ended',
[DELINQUENT]: 'Delinquent',
[DEFAULTED]: 'Defaulted',
[REFUNDED]: 'Refunded',
[EXPIRED]: 'Expired'
};
return mapping[rawStatus] || rawStatus;
},
},
computed: {
getPartnerUrl() {
return partnerId => `/about/where-kiva-works/partners/${partnerId}`;
}
},
data() {
return {
placeholders: [
{ span: 4 },
{ span: 1 },
{ span: 1, marginLeft: true },
{ span: 1, marginLeft: true },
{ span: 1, marginLeft: true },
{ span: 2, marginLeft: true },
{ span: 2 }
]
};
}
};
</script>

<style lang="postcss" scoped>
.scroll-gradient {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another pending decision that needs to be run by product/design is how we want to handle the issue of this table being wider than the container containing it for the latest table component design.

@apply tw-pointer-events-none tw-absolute tw-top-0 tw-right-0 tw-h-full;

width: 2rem;
background: linear-gradient(to left, #fff, rgb(255 255 255 / 0%));
}

.loan-image {
width: 50px;
height: 50px;
}

.team-cell {
max-width: 150px;
}
</style>
Loading
Loading