Skip to content

Commit

Permalink
Merge pull request #5707 from kiva/adding-tiered-badges-to-lending-st…
Browse files Browse the repository at this point in the history
…ats-and-public-profile-MP-1089

feat: add tiered badges to BadgesList component
  • Loading branch information
christian14b authored Nov 25, 2024
2 parents 2ba6d39 + e2414fb commit c1b9185
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 157 deletions.
104 changes: 29 additions & 75 deletions src/components/LenderProfile/LenderBadges.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<async-lender-section @visible="fetchUserAchievements">
<section v-if="completedAchievements.length > 0">
<section v-if="completedBadges.length > 0">
<h2
v-if="!isLoading"
class="data-hj-suppress"
Expand All @@ -13,92 +13,46 @@
/>
<badges-list
class="tw-my-4"
:completed-achievements="completedAchievements"
:total-possible-badges="totalPossibleBadges"
:completed-achievements="completedBadges"
:is-loading="isLoading"
/>
</section>
</async-lender-section>
</template>

<script>
import { gql } from 'graphql-tag';
import logReadQueryError from '#src/util/logReadQueryError';
<script setup>
import BadgesList from '#src/pages/Portfolio/LendingStats/BadgesList';
import KvLoadingPlaceholder from '#kv-components/KvLoadingPlaceholder';
import { computed, ref, inject } from 'vue';
import useBadgeData from '#src/composables/useBadgeData';
import AsyncLenderSection from './AsyncLenderSection';
const userAchievementProgressQuery = gql`query userAchievementProgress( $publicId: String!) {
userAchievementProgress(publicId: $publicId) {
id
lendingAchievements {
id
milestoneProgress {
id
milestoneStatus
}
}
}
}`;
export default {
name: 'LenderBadges',
inject: ['apollo', 'cookieStore'],
components: {
BadgesList,
KvLoadingPlaceholder,
AsyncLenderSection,
},
props: {
lenderInfo: {
type: Object,
default: () => ({})
},
publicId: {
type: String,
required: true,
},
},
data() {
return {
isLoading: true,
allAchievements: [],
};
const props = defineProps({
lenderInfo: {
type: Object,
default: () => ({})
},
computed: {
lenderName() {
return this.lenderInfo?.name ?? '';
},
badgesTitle() {
return this.lenderInfo?.name
? `${this.lenderInfo.name}'s badges`
: 'Badges';
},
completedAchievements() {
return this.allAchievements.filter(
achievement => achievement.milestoneProgress?.[0]?.milestoneStatus === 'COMPLETE'
);
},
totalPossibleBadges() {
return this.allAchievements?.length ?? 0;
},
publicId: {
type: String,
required: true,
},
methods: {
async fetchUserAchievements() {
try {
const { data } = await this.apollo.query({
query: userAchievementProgressQuery,
variables: {
publicId: this.publicId,
},
});
});
this.allAchievements = data?.userAchievementProgress?.lendingAchievements ?? [];
this.isLoading = false;
} catch (e) {
logReadQueryError(e, 'LenderBadges userAchievementsProgress');
}
},
},
const apollo = inject('apollo');
const {
fetchAchievementData,
fetchContentfulData,
completedBadges,
} = useBadgeData(apollo);
const isLoading = ref(true);
const badgesTitle = computed(() => (props.lenderInfo?.name ? `${props.lenderInfo.name}'s badges` : 'Badges'));
const fetchUserAchievements = async () => {
await Promise.all([
fetchAchievementData(apollo, props.publicId),
fetchContentfulData(apollo),
]);
isLoading.value = false;
};
</script>
61 changes: 58 additions & 3 deletions src/composables/useBadgeData.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ export default function useBadgeData() {
* Calls Apollo to get the badge achievement service data
*
* @param apollo The current instance of Apollo
* @param publicId Whether to get achievement data for a specific user
*/
const fetchAchievementData = apollo => {
apollo.query({ query: userAchievementProgressQuery })
const fetchAchievementData = (apollo, publicId = null) => {
apollo.query({ query: userAchievementProgressQuery, variables: { publicId } })
.then(result => {
badgeAchievementData.value = [
...(result.data?.userAchievementProgress?.lendingAchievements ?? []),
Expand Down Expand Up @@ -343,7 +344,7 @@ export default function useBadgeData() {
levelName: contentfulData.challengeName,
};
}
} else if (badge.achievementData?.tiers?.length) {
} else if (badge?.achievementData?.tiers?.length) {
const tiers = JSON.parse(JSON.stringify(badge.achievementData.tiers));
tiers.sort((a, b) => new Date(a.completedDate) - new Date(b.completedDate));
const levelIndex = tiers[0].level - 1;
Expand Down Expand Up @@ -388,6 +389,58 @@ export default function useBadgeData() {
return displayedBadge ?? {};
};

/**
* Get completed badges of a user
*
* @param badges The badges to get the completed badges from
* @returns Completed badges
*/
const getCompletedBadges = badges => {
const completedBadgesArr = [];

badges?.forEach(badge => {
if (badge?.achievementData?.tiers?.length) {
const { tiers } = badge.achievementData;
tiers.forEach(tier => {
if (tier.completedDate) {
completedBadgesArr.push({
...badge,
earnedAtDate: tier.completedDate,
level: tier.level,
});
}
});
}
if (badge?.achievementData?.milestoneProgress?.length) {
const earnedAtDate = badge.achievementData?.milestoneProgress?.[0]?.earnedAtDate;
if (earnedAtDate) {
completedBadgesArr.push({
...badge,
earnedAtDate,
level: 0,
});
}
}
});

return completedBadgesArr;
};

/**
* Get completed badges sorted by earned date
*
* @returns Completed badges sorted by earned date
*/
const completedBadges = computed(() => {
const completedBadgesArr = getCompletedBadges(badgeData.value);

completedBadgesArr.sort((a, b) => {
return new Date(a.earnedAtDate) - new Date(b.earnedAtDate);
});

return completedBadgesArr;
});

return {
fetchAchievementData,
fetchContentfulData,
Expand All @@ -400,9 +453,11 @@ export default function useBadgeData() {
getBadgeWithVisibleTiers,
getLastCompletedBadgeLevelData,
getHighestPriorityDisplayBadge,
getCompletedBadges,
badgeAchievementData,
badgeData,
badgeLoanIdData,
isBadgeKeyValid,
completedBadges,
};
}
4 changes: 2 additions & 2 deletions src/graphql/query/userAchievementProgress.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
query UserAchievementProgress {
userAchievementProgress {
query UserAchievementProgress($publicId: String) {
userAchievementProgress(publicId: $publicId) {
id
lendingAchievements {
id
Expand Down
88 changes: 27 additions & 61 deletions src/pages/Portfolio/LendingStats/BadgesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,30 @@
</p>
<div
v-else
v-for="(challenge, index) in challengeDataContentful"
:key="index"
v-for="badge in completedAchievements"
:key="badge.id"
class="tw-flex tw-w-full tw-gap-1"
:class="{'tw-items-center': !inPortfolio }"
>
<img :src="challenge.badgeSvg" class="tw-h-10 tw-flex-none tw-mx-auto">
<img
:src="getBadgeImgUrl(badge)"
class="tw-h-10 tw-flex-none tw-mx-auto"
>
<div class="tw-w-full">
<span class="tw-font-medium">{{ challenge.challengeName }}</span>
<p v-if="shouldShowBadgeDate(challenge)" class="tw-text-secondary tw-text-small">
{{ challenge.dateTagline }}
<span class="tw-font-medium">
{{ getBadgeTitle(badge) }}
</span>
<p class="tw-text-secondary tw-text-small">
{{ getBadgeDate(badge) }}
</p>
</div>
</div>
</div>
</template>

<script>
import { formatMediaAssetArray } from '#src/util/contentfulUtils';
import { gql } from 'graphql-tag';
import KvLoadingPlaceholder from '#kv-components/KvLoadingPlaceholder';
import { format } from 'date-fns';
export default {
name: 'BadgesList',
Expand All @@ -54,72 +58,34 @@ export default {
type: Array,
default: () => ([])
},
totalPossibleBadges: {
type: Number,
default: 0
},
isLoading: {
type: Boolean,
default: false
},
},
data() {
return {
challengeDataContentful: [],
};
},
computed: {
/* Challenge keys for completed challenges */
completedChallengeKeys() {
return this.completedAchievements.map(achievement => achievement.id).join(',');
},
inPortfolio() {
return this.$route?.name?.includes('portfolio');
},
},
watch: {
// When this prop has loaded we can load the badges content from contentful
totalPossibleBadges: {
handler(next) {
if (next !== 0) {
this.loadBadgesContent();
}
},
immediate: true
}
},
methods: {
loadBadgesContent() {
if (this.completedChallengeKeys !== '') {
const filterFields = `fields.key[in]=${this.completedChallengeKeys}`;
this.apollo.query({
query: gql`query contentfulDefinitions($customFields: String) {
contentful {
entries(contentType: "challenge", customFields: $customFields)
}
}`,
variables: {
customFields: filterFields
},
}).then(result => {
const contentfulData = result.data?.contentful?.entries?.items ?? null;
if (contentfulData) {
this.challengeDataContentful = contentfulData.map(item => {
return {
...item.fields,
badgeSvg: formatMediaAssetArray([item.fields.badgeImage])?.[0]?.file?.url,
};
});
}
}).catch(err => {
console.error(err);
this.$showTipMsg('There was a problem loading badges', 'error');
});
getBadgeTitle(badge) {
if (badge.level === 0) {
return badge?.contentfulData?.[0]?.challengeName ?? '';
}
const badgeData = badge?.contentfulData?.find(data => data.level === badge.level);
return `${badgeData?.challengeName} ${badgeData?.levelName}` ?? '';
},
getBadgeImgUrl(badge) {
if (badge.level === 0) {
return badge?.contentfulData?.[0]?.imageUrl ?? '';
}
const badgeData = badge?.contentfulData?.find(data => data.level === badge.level);
return badgeData?.imageUrl ?? '';
},
shouldShowBadgeDate(challenge) {
// Lifetime badges currently have an "n/a" date tagline
return challenge?.dateTagline?.toLowerCase() !== 'n/a';
getBadgeDate(badge) {
const earnedAtDate = badge.earnedAtDate ? Date.parse(badge.earnedAtDate) : new Date();
return format(earnedAtDate, 'MMM yyyy');
},
},
};
Expand Down
13 changes: 4 additions & 9 deletions src/pages/Portfolio/LendingStats/BadgesSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
v-if="!isLoading"
class="tw-text-base tw-bg-brand tw-text-white tw-py-0.5 tw-px-1 tw-self-center"
>
{{ badgesObtained }}/{{ totalPossibleBadges }}
{{ badgesObtained }}
</span>
<kv-loading-placeholder
v-else
Expand All @@ -16,7 +16,6 @@
</h2>
<badges-list
:completed-achievements="completedAchievements"
:total-possible-badges="totalPossibleBadges"
:is-loading="isLoading"
/>
</section>
Expand All @@ -39,10 +38,9 @@ export default {
type: Array,
default: () => []
},
/* total number of possible badges */
totalPossibleBadges: {
type: Number,
default: 0
isLoading: {
type: Boolean,
default: false
},
},
components: {
Expand All @@ -53,9 +51,6 @@ export default {
badgesObtained() {
return this.completedAchievements.length ?? 0;
},
isLoading() {
return this.totalPossibleBadges === 0;
},
},
};
</script>
Loading

0 comments on commit c1b9185

Please sign in to comment.