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 e59c87c240..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
-
diff --git a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap index cf81e04dbe..f3b861cd91 100644 --- a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap @@ -5,9 +5,6 @@ exports[`Matches shallow shapshot 1 shapshot 1 1`] = ` className="src-shared-components-challenge-listing-___style__ChallengeFiltersExample___3IjeI" id="challengeFilterContainer" > -
@@ -24,35 +21,41 @@ exports[`Matches shallow shapshot 1 shapshot 1 1`] = ` setFilterState={[MockFunction]} />
- +
+ + +
`; @@ -62,9 +65,6 @@ exports[`Matches shallow shapshot 2 shapshot 2 1`] = ` className="src-shared-components-challenge-listing-___style__ChallengeFiltersExample___3IjeI" id="challengeFilterContainer" > -
@@ -81,35 +81,41 @@ exports[`Matches shallow shapshot 2 shapshot 2 1`] = ` setFilterState={[MockFunction]} />
- +
+ + +
`; diff --git a/src/assets/images/icon-circle.png b/src/assets/images/icon-circle.png new file mode 100644 index 0000000000..e4d6ffea8b Binary files /dev/null and b/src/assets/images/icon-circle.png differ diff --git a/src/assets/images/icon-computer.svg b/src/assets/images/icon-computer.svg new file mode 100644 index 0000000000..84218de143 --- /dev/null +++ b/src/assets/images/icon-computer.svg @@ -0,0 +1,17 @@ + + + Group 23 + + + + + + + + + + + + + + \ 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..c4251e52eb --- /dev/null +++ b/src/assets/images/icon-verified.svg @@ -0,0 +1,37 @@ + + + Group 6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 725a6bec92..bce1df1e32 100644 --- a/src/shared/actions/challenge-listing/index.js +++ b/src/shared/actions/challenge-listing/index.js @@ -53,7 +53,8 @@ const REVIEW_OPPORTUNITY_PAGE_SIZE = 1000; */ function getChallengeTypesDone() { return getService() - .getChallengeTypes(); + .getChallengeTypes() + .then(res => res.sort((a, b) => a.name.localeCompare(b.name))); } /** @@ -90,10 +91,6 @@ function getAllChallengesInit(uuid, page, frontFilter) { return { uuid, page, frontFilter }; } -function getRecommendedChallengesInit(uuid, page, frontFilter) { - return { uuid, page, frontFilter }; -} - function getMyPastChallengesInit(uuid, page, frontFilter) { return { uuid, page, frontFilter }; } @@ -231,8 +228,22 @@ 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 = { @@ -249,6 +260,15 @@ function getOpenForRegistrationChallengesDone(uuid, page, backendFilter, }; 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, @@ -305,17 +325,6 @@ function getAllChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = })); } -function getRecommendedChallengesDone(uuid, tokenV3, sort, filter) { - const service = getService(tokenV3); - return service.getRecommendedChallenges(sort, filter).then(ch => ({ - uuid, - recommendedChallenges: ch.challenges, - meta: { - allRecommendedChallengesCount: ch.meta, - }, - })); -} - function getMyPastChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = {}) { const userId = decodeToken(tokenV3).userId.toString(); const { sorts } = frontFilter; @@ -551,9 +560,6 @@ export default createActions({ GET_ALL_CHALLENGES_INIT: getAllChallengesInit, GET_ALL_CHALLENGES_DONE: getAllChallengesDone, - GET_RECOMMENDED_CHALLENGES_INIT: getRecommendedChallengesInit, - GET_RECOMMENDED_CHALLENGES_DONE: getRecommendedChallengesDone, - GET_ACTIVE_CHALLENGES_INIT: getActiveChallengesInit, GET_ACTIVE_CHALLENGES_DONE: getActiveChallengesDone, diff --git a/src/shared/components/challenge-detail/Header/ChallengeTags.jsx b/src/shared/components/challenge-detail/Header/ChallengeTags.jsx index ce6190cc3a..7f7943fc56 100644 --- a/src/shared/components/challenge-detail/Header/ChallengeTags.jsx +++ b/src/shared/components/challenge-detail/Header/ChallengeTags.jsx @@ -6,6 +6,7 @@ */ +import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; @@ -22,15 +23,22 @@ import { } from 'topcoder-react-ui-kit'; import { COMPETITION_TRACKS } from 'utils/tc'; +import VerifiedIcon from 'assets/images/icon-verified.svg'; +import MatchScore from 'components/challenge-listing/ChallengeCard/MatchScore'; +import Tooltip from 'components/Tooltip'; +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,19 @@ 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)); + + const verifiedTagTooltip = item => ( +
+

{item} is verified based
on past challenges you won

+
+ ); + return (
{ @@ -83,7 +104,33 @@ export default function ChallengeTags(props) { )) } { - technPlatforms.map(tag => ( + matchScore && ( + + ) + } + { + matchSkills.map(item => ( +
+ + + + {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..783e8b8101 100644 --- a/src/shared/components/challenge-detail/Header/style.scss +++ b/src/shared/components/challenge-detail/Header/style.scss @@ -356,3 +356,38 @@ position: relative; top: -5px; } + +.verified-tag { + position: absolute; +} + +.verified-tag-text { + margin-left: 15px; +} + +.recommended-challenge-tooltip { + display: inline-block; + margin: 3px 0 0 0; +} + +.tctooltiptext { + background: $tc-white; + color: $tc-gray-90; + border-radius: 3px; + padding: 10px; +} + +.tctooltiptext::after { + content: ""; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + bottom: 4px; + margin-left: -5px; + border-width: 5px 5px 0; + left: 50%; + border-top-color: $tc-white; + z-index: 1000; +} diff --git a/src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx index 4e7296408d..9502ef8eb8 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx @@ -1,13 +1,15 @@ 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 - +
+ + {score}% match + +
); } diff --git a/src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss index 563eebd007..0f4561d569 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss +++ b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss @@ -1,12 +1,7 @@ @import "~styles/mixins"; -.match-score { - background-color: $tc-light-blue-30; - color: $tc-dark-blue-100; - font-size: 10px; - line-height: 12px; - padding: 3px; - border-radius: 2px; - margin-left: 5px; - margin-top: 1px; +.matchScoreTag { + margin-right: 4px; + 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 ac8c1941cd..8ffc8fb7de 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/index.jsx @@ -49,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 (
@@ -77,17 +78,32 @@ function ChallengeCard({ openNewTab={openChallengesInNewTabs} >

{challenge.name}

- { - challenge.matchScore - && - }
{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 && ( item.abbreviation !== 'REC'); const isRecommendedChallengesVisible = activeBucket === 'openForRegistration'; const [recommendedToggle, setRecommendedToggle] = useState(false); @@ -264,37 +265,43 @@ export default function FiltersPanel({ types: _.uniq(filterState.types), }); } + + if (filterState.recommended) { + setRecommendedToggle(true); + } }, [filterState]); const onSwitchRecommendedChallenge = (on) => { - const { types } = filterState; - types.push('REC'); - setRecommendedToggle(on); + setFilterState({ ..._.clone(filterState), recommended: on }); + selectBucket(BUCKETS.OPEN_FOR_REGISTRATION); if (on) { setSort('openForRegistration', 'bestMatch'); - setFilterState({ ..._.clone(filterState), types }); - } else { - setSort('openForRegistration', 'startDate'); - setFilterState({ ..._.clone(filterState), types: types.filter(item => item !== 'REC') }); - } - }; - - const handleTypeChange = (option, e) => { - let { types } = filterState; - if (e.target.checked) { - types = types.concat(option.value); - } else { - types = types.filter(type => type !== option.value); - } - - if (recommendedToggle) { - types = _.union(types, ['REC']); + 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 { - types = types.filter(type => type !== 'REC'); setSort('openForRegistration', 'startDate'); + setFilterState({ + ...filterState, + recommended: false, + }); } - setFilterState({ ..._.clone(filterState), types }); + setRecommendedToggle(on); }; const recommendedCheckboxTip = ( @@ -466,11 +473,11 @@ export default function FiltersPanel({
- Challenge Type + Type
{ - availableTypes + validTypes .map(mapTypes) .map(option => ( @@ -480,7 +487,17 @@ export default function FiltersPanel({ name={option.label} id={option.label} checked={filterState.types.includes(option.value)} - onChange={e => handleTypeChange(option, e)} + onChange={(e) => { + let { types } = filterState; + + if (e.target.checked) { + types = types.concat(option.value); + } else { + types = types.filter(type => type !== option.value); + } + + setFilterState({ ..._.clone(filterState), types }); + }} /> @@ -584,7 +601,7 @@ export default function FiltersPanel({ } { - isRecommendedChallengesVisible + isRecommendedChallengesVisible && _.get(auth, 'user.userId') && (
-