diff --git a/.circleci/config.yml b/.circleci/config.yml
index c94aa0f975..15009f5042 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -276,6 +276,7 @@ workflows:
branches:
only:
- develop
+ - feature/recommender-sync-develop
# This is alternate dev env for parallel testing
- "build-test":
context : org-global
@@ -289,7 +290,7 @@ workflows:
filters:
branches:
only:
- - gigs-housekeep
+ - free
# This is beta env for production soft releases
- "build-prod-beta":
context : org-global
@@ -304,6 +305,7 @@ workflows:
branches:
only:
- develop
+ - feature/recommender-sync-develop
- "approve-smoke-test-on-staging":
type: approval
requires:
diff --git a/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap b/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap
index 5297459e04..884185c3f9 100644
--- a/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap
+++ b/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap
@@ -137,7 +137,7 @@ exports[`Matches shallow shapshot 2`] = `
- Challenge Type
+ Type
+
\ No newline at end of file
diff --git a/src/assets/images/icon-verified.svg b/src/assets/images/icon-verified.svg
new file mode 100644
index 0000000000..a38fcd7263
--- /dev/null
+++ b/src/assets/images/icon-verified.svg
@@ -0,0 +1,37 @@
+
+
\ No newline at end of file
diff --git a/src/shared/actions/challenge-listing/index.js b/src/shared/actions/challenge-listing/index.js
index 346e1adc1b..bce1df1e32 100644
--- a/src/shared/actions/challenge-listing/index.js
+++ b/src/shared/actions/challenge-listing/index.js
@@ -228,9 +228,24 @@ function getActiveChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter
// }));
}
+/**
+ * Gets open for registration challenges
+ * @param {String} uuid
+ * @param {Number} page
+ * @param {Object} backendFilter Backend filter to use.
+ * @param {String} tokenV3 Optional. Topcoder auth token v3. Without token only
+ * public challenges will be fetched. With the token provided, the action will
+ * also fetch private challenges related to this user.
+ * @param {Object} frontFilter
+ * @param {boolean} recommended recommended toggle is on or off
+ * @param {String} handle user handle
+
+ * @return {Promise}
+ */
function getOpenForRegistrationChallengesDone(uuid, page, backendFilter,
- tokenV3, frontFilter = {}) {
+ tokenV3, frontFilter = {}, recommended = false, handle) {
const { sorts } = frontFilter;
+ const sortOrder = SORT[sorts[BUCKETS.OPEN_FOR_REGISTRATION]];
const filter = {
backendFilter,
frontFilter: {
@@ -240,11 +255,20 @@ function getOpenForRegistrationChallengesDone(uuid, page, backendFilter,
perPage: PAGE_SIZE,
page: page + 1,
sortBy: sorts[BUCKETS.OPEN_FOR_REGISTRATION],
- sortOrder: SORT[sorts[BUCKETS.OPEN_FOR_REGISTRATION]].order,
+ sortOrder: sortOrder ? sortOrder.order : 'asc',
},
};
delete filter.frontFilter.sorts;
const service = getService(tokenV3);
+ if (recommended) {
+ return service.getRecommendedChallenges(filter, handle).then(ch => ({
+ uuid,
+ openForRegistrationChallenges: ch.challenges,
+ meta: ch.meta,
+ frontFilter,
+ }));
+ }
+
return service.getChallenges(filter).then(ch => ({
uuid,
openForRegistrationChallenges: ch.challenges,
@@ -522,6 +546,7 @@ export default createActions({
DROP_MY_CHALLENGES: _.noop,
DROP_ALL_CHALLENGES: _.noop,
DROP_PAST_CHALLENGES: _.noop,
+ DROP_RECOMMENDED_CHALLENGES: _.noop,
// GET_ALL_ACTIVE_CHALLENGES_INIT: getAllActiveChallengesInit,
// GET_ALL_ACTIVE_CHALLENGES_DONE: getAllActiveChallengesDone,
diff --git a/src/shared/components/challenge-detail/Header/ChallengeTags.jsx b/src/shared/components/challenge-detail/Header/ChallengeTags.jsx
index ce6190cc3a..e0be111124 100644
--- a/src/shared/components/challenge-detail/Header/ChallengeTags.jsx
+++ b/src/shared/components/challenge-detail/Header/ChallengeTags.jsx
@@ -6,8 +6,10 @@
*/
+import _ from 'lodash';
import React from 'react';
import PT from 'prop-types';
+import { config } from 'topcoder-react-utils';
import {
Tag,
@@ -22,15 +24,21 @@ import {
} from 'topcoder-react-ui-kit';
import { COMPETITION_TRACKS } from 'utils/tc';
+import VerifiedTag from 'components/challenge-listing/VerifiedTag';
+import MatchScore from 'components/challenge-listing/ChallengeCard/MatchScore';
+import { calculateScore } from '../../../utils/challenge-listing/helper';
+import './style.scss';
export default function ChallengeTags(props) {
const {
+ challengeId,
challengesUrl,
track,
challengeType,
events,
technPlatforms,
setChallengeListingFilter,
+ openForRegistrationChallenges,
} = props;
let EventTag;
@@ -56,6 +64,13 @@ export default function ChallengeTags(props) {
throw new Error('Wrong competition track value');
}
+
+ const filteredChallenge = _.find(openForRegistrationChallenges, { id: challengeId });
+ const matchSkills = filteredChallenge ? filteredChallenge.match_skills || [] : [];
+ const matchScore = filteredChallenge ? filteredChallenge.jaccard_index || [] : 0;
+
+ const tags = technPlatforms.filter(tag => !matchSkills.includes(tag));
+
return (
{
@@ -83,7 +98,22 @@ export default function ChallengeTags(props) {
))
}
{
- technPlatforms.map(tag => (
+ matchScore > 0 && config.ENABLE_RECOMMENDER && (
+
+
+
+ )
+ }
+ {
+ matchSkills.map(item => (
+
+ ))
+ }
+ {
+ tags.map(tag => (
tag
&& (
{(hasRecommendedChallenges || hasThriveArticles) && (
@@ -505,6 +508,7 @@ ChallengeHeader.propTypes = {
phases: PT.any,
roundId: PT.any,
prizeSets: PT.any,
+ match_skills: PT.arrayOf(PT.string),
}).isRequired,
challengesUrl: PT.string.isRequired,
hasRegistered: PT.bool.isRequired,
@@ -525,4 +529,5 @@ ChallengeHeader.propTypes = {
hasFirstPlacement: PT.bool.isRequired,
isMenuOpened: PT.bool,
mySubmissions: PT.arrayOf(PT.shape()).isRequired,
+ openForRegistrationChallenges: PT.shape().isRequired,
};
diff --git a/src/shared/components/challenge-detail/Header/style.scss b/src/shared/components/challenge-detail/Header/style.scss
index f78bd1c624..2e4d9c7064 100644
--- a/src/shared/components/challenge-detail/Header/style.scss
+++ b/src/shared/components/challenge-detail/Header/style.scss
@@ -356,3 +356,9 @@
position: relative;
top: -5px;
}
+
+.matchScoreWrap {
+ width: 100%;
+ margin-right: -2px;
+ padding: 0;
+}
diff --git a/src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx
new file mode 100644
index 0000000000..9502ef8eb8
--- /dev/null
+++ b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx
@@ -0,0 +1,22 @@
+import PT from 'prop-types';
+import React from 'react';
+import { DevelopmentTrackEventTag } from 'topcoder-react-ui-kit';
+import './style.scss';
+
+export default function MatchScore({ score }) {
+ return (
+
+
+ {score}% match
+
+
+ );
+}
+
+MatchScore.defaultProps = {
+ score: 0,
+};
+
+MatchScore.propTypes = {
+ score: PT.number,
+};
diff --git a/src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss
new file mode 100644
index 0000000000..80d12a53cb
--- /dev/null
+++ b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss
@@ -0,0 +1,7 @@
+@import "~styles/mixins";
+
+.matchScoreTag {
+ margin-right: 0;
+ margin-bottom: 4px;
+ display: inline-block;
+}
diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx
index d3315863aa..0d91283aa5 100644
--- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx
+++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx
@@ -312,6 +312,6 @@ ChallengeStatus.propTypes = {
openChallengesInNewTabs: PT.bool, // eslint-disable-line react/no-unused-prop-types
selectChallengeDetailsTab: PT.func.isRequired,
className: PT.string,
- userId: PT.string,
+ userId: PT.number,
isLoggedIn: PT.bool.isRequired,
};
diff --git a/src/shared/components/challenge-listing/ChallengeCard/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/index.jsx
index ab1dec84e4..8ffc8fb7de 100644
--- a/src/shared/components/challenge-listing/ChallengeCard/index.jsx
+++ b/src/shared/components/challenge-listing/ChallengeCard/index.jsx
@@ -13,6 +13,8 @@ import Tags from '../Tags';
import ChallengeStatus from './Status';
import TrackAbbreviationTooltip from '../Tooltips/TrackAbbreviationTooltip';
+import MatchScore from './MatchScore';
+import { calculateScore } from '../../../utils/challenge-listing/helper';
import './style.scss';
/* TODO: Note that this component uses a dirty trick to cheat linter and to be
@@ -47,6 +49,7 @@ function ChallengeCard({
const registrationPhase = (challenge.phases || []).filter(phase => phase.name === 'Registration')[0];
const isRegistrationOpen = registrationPhase ? registrationPhase.isOpen : false;
+ const isRecommendedChallenge = challenge.jaccard_index;
return (
@@ -67,19 +70,40 @@ function ChallengeCard({
-
selectChallengeDetailsTab(DETAIL_TABS.DETAILS)}
- to={challengeDetailLink}
- styleName="challenge-title"
- openNewTab={openChallengesInNewTabs}
- >
{challenge.name}
-
+
+
selectChallengeDetailsTab(DETAIL_TABS.DETAILS)}
+ to={challengeDetailLink}
+ styleName="challenge-title"
+ openNewTab={openChallengesInNewTabs}
+ >
{challenge.name}
+
+
{challenge.status === 'Active' ? 'Ends ' : 'Ended '}
{getEndDate(challenge)}
- { challenge.tags.length > 0
+ {
+ isRecommendedChallenge
+ &&
+ }
+ {
+ isRecommendedChallenge
+ && challenge.match_skills.length > 0
+ && (
+
expandTag(challenge.id)}
+ verifiedTags={challenge.match_skills}
+ recommended
+ />
+ )
+ }
+ { !isRecommendedChallenge
+ && challenge.tags.length > 0
&& (
{
+ if (!isFilterEmpty(filterState, past ? 'past' : '', activeBucket)
+ && recommendedToggle
+ && filterState.types.length !== _.uniq(filterState.types).length
+ ) {
+ setFilterState({
+ ...filterState,
+ types: _.uniq(filterState.types),
+ });
+ }
+
+ if (filterState.recommended) {
+ setRecommendedToggle(true);
+ }
+ }, [filterState]);
+
+ const onSwitchRecommendedChallenge = (on) => {
+ setFilterState({ ..._.clone(filterState), recommended: on });
+ selectBucket(BUCKETS.OPEN_FOR_REGISTRATION);
+
+ if (on) {
+ setSort('openForRegistration', 'bestMatch');
+ setFilterState({
+ ...filterState,
+ tracks: {
+ Dev: true,
+ Des: true,
+ DS: true,
+ QA: true,
+ },
+ search: '',
+ tags: [],
+ types: ['CH', 'F2F', 'TSK'],
+ groups: [],
+ events: [],
+ endDateStart: null,
+ startDateEnd: null,
+ recommended: true,
+ });
+ } else {
+ setSort('openForRegistration', 'startDate');
+ setFilterState({
+ ...filterState,
+ recommended: false,
+ });
+ }
+ setRecommendedToggle(on);
+ };
+
+ const recommendedCheckboxTip = (
+
+
Shows available challenges
that match your skills
+
+ );
+
return (
@@ -412,7 +473,7 @@ export default function FiltersPanel({
- Challenge Type
+ Type
{
@@ -494,7 +555,7 @@ export default function FiltersPanel({
) : null
}
- { !isReviewOpportunitiesBucket
+ { !isReviewOpportunitiesBucket && !(recommendedToggle && activeBucket === 'openForRegistration')
&& (
@@ -538,13 +599,51 @@ export default function FiltersPanel({
)
}
+
+ {
+ isRecommendedChallengesVisible && _.get(auth, 'user.userId')
+ && (
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+ {
+ isRecommendedChallengesVisible && _.get(auth, 'user.userId')
+ && (
)
+ }
+