diff --git a/config/dev-custom-host.js b/config/dev-custom-host.js
new file mode 100644
index 0000000000..1ce6977f63
--- /dev/null
+++ b/config/dev-custom-host.js
@@ -0,0 +1,37 @@
+const { merge } = require('webpack-merge');
+var base = require('./index.js')
+var devVm = require('./dev-vm.js')
+
+module.exports = merge(base, devVm, {
+ app: {
+ apolloBatching: false,
+ host: 'kiva-ui.local:8888',
+ publicPath: '/',
+ photoPath: 'https://www.development.kiva.org/img/',
+ graphqlUri: 'https://gateway.development.kiva.org/graphql',
+ enableAnalytics: false,
+ enableSnowplow: false,
+ snowplowUri: 'events.fivetran.com/snowplow/v5qt54ocr2nm',
+ enableGA: false,
+ gaId: 'UA-11686022-7', // dev-vm property
+ enableSentry: false,
+ sentryURI: 'https://7ce141b23c4a4e6091c206d08442f0e9@o7540.ingest.sentry.io/1201287',
+ auth0: {
+ loginRedirectUrls: {
+ xOXldYg02WsLnlnn0D5xoPWI2i3aNsFD: 'https://www.development.kiva.org/authenticate?authLevel=recent',
+ KIzjUBQjKZwMRgYSn6NvMxsUwNppwnLH: 'http://kiva-ui.local:8888/ui-login?force=true',
+ ouGKxT4mE4wQEKqpfsHSE96c9rHXQqZF: 'http://kiva-ui.local:8888/ui-login?force=true',
+ },
+ enable: true,
+ browserCallbackUri: 'http://kiva-ui.local:8888/process-browser-auth',
+ serverCallbackUri: 'http://kiva-ui.local:8888/process-ssr-auth',
+ apiAudience: 'https://gateway.development.kiva.org/graphql',
+ },
+ },
+ server: {
+ graphqlUri: 'https://gateway.development.kiva.org/graphql',
+ sessionUri: 'https://www.development.kiva.org/start-ui-session',
+ memcachedEnabled: false,
+ disableCluster: true,
+ }
+})
diff --git a/package.json b/package.json
index c6e5796bee..e41a3d9d52 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"fetchSchema": "node build/fetch-schema.js && wait-on build/schema.graphql",
"report": "webpack-bundle-analyzer dist/client-bundle-stats.json",
"unit": "jest --config test/unit/jest.conf.js --coverage",
+ "unit-watch": "jest --config test/unit/jest.conf.js --watch",
"test": "npm run lint && npm run unit && npm run build && npm run test:e2e",
"test:e2e": "npm start -- --config=dev-vm-mac && wait-on tcp:8888 && npm run e2e:headless",
"coverage:merge": "node test/mergeCoverage.js",
diff --git a/src/components/Checkout/BasketItem.vue b/src/components/Checkout/BasketItem.vue
index 2cc9d443d7..e02b724773 100644
--- a/src/components/Checkout/BasketItem.vue
+++ b/src/components/Checkout/BasketItem.vue
@@ -122,8 +122,7 @@ import LoanReservation from '@/components/Checkout/LoanReservation';
import LoanPrice from '@/components/Checkout/LoanPrice';
import RemoveBasketItem from '@/components/Checkout/RemoveBasketItem';
import TeamAttribution from '@/components/Checkout/TeamAttribution';
-
-const teamChallengeCookieName = 'kv-team-challenge';
+import { getForcedTeamId, removeLoansFromChallengeCookie } from '@/util/teamChallengeUtils';
export default {
name: 'BasketItem',
@@ -197,6 +196,7 @@ export default {
this.$emit('refreshtotals', $event);
if ($event === 'removeLoan') {
this.loanVisible = false;
+ removeLoansFromChallengeCookie(this.cookieStore, [this.loan.id]);
}
},
},
@@ -207,32 +207,7 @@ export default {
}
},
mounted() {
- // Team Challenge MVP Code
- // If team challenge cookie is present, the user has added a loan to basket from the challenge page
- // In that case, append the team info to the list of teams and attribute this loan to that team
- if (this.cookieStore.get(teamChallengeCookieName)) {
- const teamChallengeLoanData = JSON.parse(this.cookieStore.get(teamChallengeCookieName));
- teamChallengeLoanData.forEach(loan => {
- if (loan.loanId === this.loan.id) {
- // Loan has a different team attribution, we should override the default
- // Is team not in the users list, append it
- if (!this.combinedTeams.some(team => team.id === loan.teamId)) {
- this.appendedTeams.push({
- id: loan.teamId,
- name: loan.teamName
- });
- }
- this.forceTeamId = loan.teamId;
- }
- });
- // Remove this loan from the cookie object after we've used it
- teamChallengeLoanData.splice(
- teamChallengeLoanData.findIndex(loan => loan.loanId === this.loan.id),
- 1
- );
- // overwrite the cookie with the new data
- this.cookieStore.set(teamChallengeCookieName, JSON.stringify(teamChallengeLoanData));
- }
+ this.forceTeamId = getForcedTeamId(this.cookieStore, this.loan.id, this.combinedTeams, this.appendedTeams);
}
};
diff --git a/src/components/Teams/TeamGoal.vue b/src/components/Teams/TeamGoal.vue
index 5e9d367b2b..697195a566 100644
--- a/src/components/Teams/TeamGoal.vue
+++ b/src/components/Teams/TeamGoal.vue
@@ -37,12 +37,39 @@
-
{{ membersParticipating }} members participating
+
+
+
+
+
+ {{ participationTotalCount }} members participating
+
+
View
@@ -55,6 +82,7 @@
import TeamInfoFromId from '@/graphql/query/teamInfoFromId.graphql';
import teamNoImage from '@/assets/images/team_s135.png';
import teamGoalInfo from '@/plugins/team-goal-mixin';
+import { isLegacyPlaceholderAvatar } from '@/util/imageUtils';
import KvProgressBar from '~/@kiva/kv-components/vue/KvProgressBar';
import KvButton from '~/@kiva/kv-components/vue/KvButton';
import KvLoadingPlaceholder from '~/@kiva/kv-components/vue/KvLoadingPlaceholder';
@@ -84,9 +112,15 @@ export default {
teamImage() {
return this.teamImageUrl || this.teamNoImage;
},
- membersParticipating() {
+ participationTotalCount() {
return this.goal?.participation?.totalCount ?? 0;
},
+ participationLendersDisplayed() {
+ return (this.goal?.participation?.values ?? []).map(p => ({
+ ...p.lender,
+ isLegacyPlaceholder: isLegacyPlaceholderAvatar(p.lender.image.url.split('/').pop()),
+ })).slice(0, 4);
+ },
},
methods: {
fetchTeamData() {
diff --git a/src/graphql/query/teamsGoals.graphql b/src/graphql/query/teamsGoals.graphql
index 5a4fc84af0..62238aea4c 100644
--- a/src/graphql/query/teamsGoals.graphql
+++ b/src/graphql/query/teamsGoals.graphql
@@ -10,6 +10,9 @@ query GetGoals ($teamId: Int, $limit: Int) {
values {
lender {
id
+ image {
+ url
+ }
}
}
}
diff --git a/src/pages/Checkout/CheckoutPage.vue b/src/pages/Checkout/CheckoutPage.vue
index d2f7c82149..2db6e6ddda 100644
--- a/src/pages/Checkout/CheckoutPage.vue
+++ b/src/pages/Checkout/CheckoutPage.vue
@@ -324,6 +324,7 @@ import experimentAssignmentQuery from '@/graphql/query/experimentAssignment.grap
import fiveDollarsTest, { FIVE_DOLLARS_NOTES_EXP } from '@/plugins/five-dollars-test-mixin';
import FtdsMessage from '@/components/Checkout/FtdsMessage';
import FtdsDisclaimer from '@/components/Checkout/FtdsDisclaimer';
+import { removeLoansFromChallengeCookie } from '@/util/teamChallengeUtils';
import KvLoadingPlaceholder from '~/@kiva/kv-components/vue/KvLoadingPlaceholder';
import KvPageContainer from '~/@kiva/kv-components/vue/KvPageContainer';
import KvButton from '~/@kiva/kv-components/vue/KvButton';
@@ -877,6 +878,8 @@ export default {
800
);
});
+
+ removeLoansFromChallengeCookie(this.cookieStore, this.loanIdsInBasket);
},
setUpdatingTotals(state) {
this.updatingTotals = state;
diff --git a/src/util/imageUtils.js b/src/util/imageUtils.js
index ee989a5004..61564d3fc7 100644
--- a/src/util/imageUtils.js
+++ b/src/util/imageUtils.js
@@ -78,6 +78,7 @@ export function getKivaImageUrl({
* The legacy avatars are found exclusively at the following urls:
*
/img//726677.jpg
* /img//315726.jpg
+ * for images from Fastly, urls, like: /img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg
* @param {String|Number} filename or hash of avatar image
* @returns {Boolean} full url for the image
*/
@@ -92,6 +93,6 @@ export function isLegacyPlaceholderAvatar(filename) {
if (filenameCleaned.indexOf('.') > -1) {
[filenameCleaned] = filenameCleaned.split('.');
}
- const defaultProfileIds = ['726677', '315726'];
+ const defaultProfileIds = ['726677', '315726', '4d844ac2c0b77a8a522741b908ea5c32'];
return defaultProfileIds.some(id => id === filenameCleaned);
}
diff --git a/src/util/teamChallengeUtils.js b/src/util/teamChallengeUtils.js
new file mode 100644
index 0000000000..d1ce8a1857
--- /dev/null
+++ b/src/util/teamChallengeUtils.js
@@ -0,0 +1,58 @@
+export const TEAM_CHALLENGE_COOKIE_NAME = 'kv-team-challenge';
+
+const getChallengeCookieData = cookieStore => {
+ const cookie = cookieStore.get(TEAM_CHALLENGE_COOKIE_NAME);
+ if (cookie) {
+ return JSON.parse(cookie);
+ }
+};
+
+/**
+ * If team challenge cookie is present, the user has added a loan to basket from the challenge page.
+ * In that case, append the team info to the list of teams and attribute this loan to that team.
+ *
+ * @param cookieStore The object for affecting cookies
+ * @param loanId The ID of the loan to check
+ * @param combinedTeams The combined team list for the loan
+ * @param appendedTeams The extra teams added to the loan
+ * @returns The team ID possibly forced for the loan
+ */
+export const getForcedTeamId = (cookieStore, loanId, combinedTeams, appendedTeams) => {
+ const data = getChallengeCookieData(cookieStore);
+ if (Array.isArray(data)) {
+ let forcedTeamId;
+ data.forEach(loan => {
+ if (loan.loanId === loanId) {
+ // Loan has a different team attribution, we should override the default
+ // Is team not in the users list, append it
+ if (!combinedTeams.some(team => team.id === loan.teamId)) {
+ appendedTeams.push({
+ id: loan.teamId,
+ name: loan.teamName
+ });
+ forcedTeamId = loan.teamId;
+ }
+ }
+ });
+ return forcedTeamId;
+ }
+};
+
+/**
+ * Removes loans from the challenge cookie.
+ * Used after checkout.
+ *
+ * @param cookieStore The object for affecting cookies
+ * @param loanIds The IDs of the loans to check
+ */
+export const removeLoansFromChallengeCookie = (cookieStore, loanIds) => {
+ const data = getChallengeCookieData(cookieStore);
+ if (Array.isArray(data)) {
+ loanIds.forEach(loanId => {
+ // Remove this loan from the cookie object after checkout
+ data.splice(data.findIndex(loan => loan.loanId === loanId), 1);
+ });
+ // Overwrite the cookie with the new data
+ cookieStore.set(TEAM_CHALLENGE_COOKIE_NAME, JSON.stringify(data));
+ }
+};
diff --git a/test/unit/specs/util/imageUtils.spec.js b/test/unit/specs/util/imageUtils.spec.js
index 2f7322df9b..db2c7d8e6c 100644
--- a/test/unit/specs/util/imageUtils.spec.js
+++ b/test/unit/specs/util/imageUtils.spec.js
@@ -64,6 +64,7 @@ describe('imageUtils.js', () => {
expect(isLegacyPlaceholderAvatar('123abc')).toBe(false);
expect(isLegacyPlaceholderAvatar(12344)).toBe(false);
});
+
it('Returns if legacy placeholder avatar', () => {
expect(isLegacyPlaceholderAvatar('726677.jpg')).toBe(true);
expect(isLegacyPlaceholderAvatar('315726.jpg')).toBe(true);
@@ -73,6 +74,8 @@ describe('imageUtils.js', () => {
expect(isLegacyPlaceholderAvatar('315726')).toBe(true);
expect(isLegacyPlaceholderAvatar(726677)).toBe(true);
expect(isLegacyPlaceholderAvatar(315726)).toBe(true);
+ expect(isLegacyPlaceholderAvatar('4d844ac2c0b77a8a522741b908ea5c32')).toBe(true);
+ expect(isLegacyPlaceholderAvatar('4d844ac2c0b77a8a522741b908ea5c32.jpg')).toBe(true);
});
});
});
diff --git a/test/unit/specs/util/teamChallengeUtils.spec.js b/test/unit/specs/util/teamChallengeUtils.spec.js
new file mode 100644
index 0000000000..315289f005
--- /dev/null
+++ b/test/unit/specs/util/teamChallengeUtils.spec.js
@@ -0,0 +1,129 @@
+import {
+ getForcedTeamId,
+ removeLoansFromChallengeCookie,
+ TEAM_CHALLENGE_COOKIE_NAME,
+} from '@/util/teamChallengeUtils';
+
+describe('teamChallengeUtils.js', () => {
+ const mockLoans = [
+ { loanId: 123, teamId: 456, teamName: 'Team Name 1' },
+ { loanId: 234, teamId: 345, teamName: 'Team Name 2' },
+ ];
+ const mockCookieJson = JSON.stringify(mockLoans);
+
+ describe('getForcedTeamId', () => {
+ it('should use expected cookie name', () => {
+ const cookieStore = { get: jest.fn() };
+ const result = getForcedTeamId(cookieStore, 1, [], []);
+ expect(result).toEqual(undefined);
+ expect(cookieStore.get).toBeCalledTimes(1);
+ expect(cookieStore.get).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME);
+ });
+
+ it('should handle empty cookie', () => {
+ const cookieStore = { get: jest.fn() };
+ const result = getForcedTeamId(cookieStore, 1, [], []);
+ expect(result).toEqual(undefined);
+ expect(cookieStore.get).toBeCalledTimes(1);
+ });
+
+ it('should handle bad cookie value', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(JSON.stringify({})), set: jest.fn() };
+ const result = getForcedTeamId(cookieStore, [mockLoans[0].loanId]);
+ expect(result).toEqual(undefined);
+ expect(cookieStore.get).toBeCalledTimes(1);
+ });
+
+ it('should handle non-matching loan ID', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() };
+ const result = getForcedTeamId(cookieStore, -1, [], []);
+ expect(result).toEqual(undefined);
+ expect(cookieStore.get).toBeCalledTimes(1);
+ });
+
+ it('should append when team arrays are empty', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() };
+ const appendedTeams = [];
+ const result = getForcedTeamId(cookieStore, mockLoans[0].loanId, [], appendedTeams);
+ expect(result).toEqual(mockLoans[0].teamId);
+ expect(cookieStore.get).toBeCalledTimes(1);
+ expect(appendedTeams).toEqual([{ id: mockLoans[0].teamId, name: mockLoans[0].teamName }]);
+ });
+
+ it('should skip when team ID already in combined', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() };
+ const appendedTeams = [];
+ const result = getForcedTeamId(
+ cookieStore,
+ mockLoans[0].loanId,
+ [{ id: mockLoans[0].teamId }],
+ appendedTeams
+ );
+ expect(result).toEqual(undefined);
+ expect(cookieStore.get).toBeCalledTimes(1);
+ expect(appendedTeams.length).toBe(0);
+ });
+
+ it('should append when non-matching team ID', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() };
+ const appendedTeams = [];
+ const result = getForcedTeamId(cookieStore, mockLoans[0].loanId, [{ id: -1 }], appendedTeams);
+ expect(result).toEqual(mockLoans[0].teamId);
+ expect(cookieStore.get).toBeCalledTimes(1);
+ expect(appendedTeams[0]).toEqual({ id: mockLoans[0].teamId, name: mockLoans[0].teamName });
+ });
+ });
+
+ describe('removeLoansFromChallengeCookie', () => {
+ it('should use expected cookie name', () => {
+ const cookieStore = { get: jest.fn() };
+ removeLoansFromChallengeCookie(cookieStore, 1);
+ expect(cookieStore.get).toBeCalledTimes(1);
+ expect(cookieStore.get).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME);
+ });
+
+ it('should handle empty cookie', () => {
+ const cookieStore = { get: jest.fn(), set: jest.fn() };
+ removeLoansFromChallengeCookie(cookieStore, []);
+ expect(cookieStore.set).toBeCalledTimes(0);
+ });
+
+ it('should handle bad cookie value', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(JSON.stringify({})), set: jest.fn() };
+ removeLoansFromChallengeCookie(cookieStore, [mockLoans[0].loanId]);
+ expect(cookieStore.set).toBeCalledTimes(0);
+ });
+
+ it('should handle bad array', () => {
+ const cookieStore = { get: jest.fn(), set: jest.fn() };
+ removeLoansFromChallengeCookie(cookieStore, 1);
+ removeLoansFromChallengeCookie(cookieStore, {});
+ removeLoansFromChallengeCookie(cookieStore, undefined);
+ expect(cookieStore.set).toBeCalledTimes(0);
+ });
+
+ it('should handle empty loan IDs', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() };
+ removeLoansFromChallengeCookie(cookieStore, []);
+ expect(cookieStore.set).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME, mockCookieJson);
+ });
+
+ it('should handle empty array in cookie', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(JSON.stringify([])), set: jest.fn() };
+ removeLoansFromChallengeCookie(cookieStore, []);
+ expect(cookieStore.set).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME, JSON.stringify([]));
+ });
+
+ it('should remove loan from cookie', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() };
+ removeLoansFromChallengeCookie(cookieStore, [mockLoans[0].loanId]);
+ expect(cookieStore.set).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME, JSON.stringify([mockLoans[1]]));
+ });
+
+ it('should remove loans from cookie', () => {
+ const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() };
+ removeLoansFromChallengeCookie(cookieStore, [mockLoans[0].loanId, mockLoans[1].loanId]);
+ expect(cookieStore.set).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME, JSON.stringify([]));
+ });
+ });
+});