From 28d72bb0426a40be46cb3ee484d989779f07e00f Mon Sep 17 00:00:00 2001 From: Dedy Wahyudi Date: Wed, 27 Jan 2021 14:20:17 +0700 Subject: [PATCH 01/36] poc-recommender-sub-2 --- .../__snapshots__/index.jsx.snap | 2 + src/shared/actions/challenge-listing/index.js | 25 ++++- .../Filters/FiltersPanel/index.jsx | 68 +++++++++++--- .../Filters/FiltersPanel/style.scss | 8 +- .../Listing/Bucket/index.jsx | 19 +++- .../challenge-listing/Listing/index.jsx | 42 +++++++-- .../components/challenge-listing/index.jsx | 5 + .../challenge-listing/FilterPanel.jsx | 6 +- .../challenge-listing/Listing/index.jsx | 92 +++++++++++++++++-- .../reducers/challenge-listing/index.js | 45 ++++++++- src/shared/utils/challenge-listing/buckets.js | 14 +++ src/shared/utils/challenge-listing/sort.js | 6 ++ 12 files changed, 295 insertions(+), 37 deletions(-) diff --git a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap index 351bba4c01..cf81e04dbe 100644 --- a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap @@ -40,6 +40,7 @@ exports[`Matches shallow shapshot 1 shapshot 1 1`] = ` loadMoreOnGoing={null} loadMoreOpenForRegistration={null} loadMorePast={null} + loadMoreRecommended={null} loadMoreReviewOpportunities={null} loadingPastChallenges={false} newChallengeDetails={false} @@ -96,6 +97,7 @@ exports[`Matches shallow shapshot 2 shapshot 2 1`] = ` loadMoreOnGoing={null} loadMoreOpenForRegistration={null} loadMorePast={null} + loadMoreRecommended={null} loadMoreReviewOpportunities={null} loadingPastChallenges={false} newChallengeDetails={false} diff --git a/src/shared/actions/challenge-listing/index.js b/src/shared/actions/challenge-listing/index.js index 346e1adc1b..725a6bec92 100644 --- a/src/shared/actions/challenge-listing/index.js +++ b/src/shared/actions/challenge-listing/index.js @@ -53,8 +53,7 @@ const REVIEW_OPPORTUNITY_PAGE_SIZE = 1000; */ function getChallengeTypesDone() { return getService() - .getChallengeTypes() - .then(res => res.sort((a, b) => a.name.localeCompare(b.name))); + .getChallengeTypes(); } /** @@ -91,6 +90,10 @@ 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,6 +234,7 @@ function getActiveChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter function getOpenForRegistrationChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = {}) { const { sorts } = frontFilter; + const sortOrder = SORT[sorts[BUCKETS.OPEN_FOR_REGISTRATION]]; const filter = { backendFilter, frontFilter: { @@ -240,7 +244,7 @@ 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; @@ -301,6 +305,17 @@ 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; @@ -522,6 +537,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, @@ -535,6 +551,9 @@ 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-listing/Filters/FiltersPanel/index.jsx b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx index e7966c7064..4771035e5d 100644 --- a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx +++ b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx @@ -64,6 +64,7 @@ export default function FiltersPanel({ // isSavingFilter, expanded, setExpanded, + setSort, }) { if (hidden && !expanded) { return ( @@ -249,6 +250,39 @@ export default function FiltersPanel({ const past = isPastBucket(activeBucket); const disableClearFilterButtons = isFilterEmpty(filterState, past ? 'past' : '', activeBucket); + const availableTypes = activeBucket === 'openForRegistration' + ? validTypes : validTypes.filter(item => item.abbreviation !== '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 (option.label === 'Recommended') { + types = types.filter(type => type === 'REC'); + if (!e.target.checked) { + setFilterState({ ..._.clone(filterState), types: ['TSK', 'CH', 'F2F'] }); + setSort('openForRegistration', 'startDate'); + } else { + setSort('openForRegistration', 'updatedBy'); + setFilterState({ ..._.clone(filterState), types }); + } + } else { + types = types.filter(type => type !== 'REC'); + setFilterState({ ..._.clone(filterState), types }); + setSort('openForRegistration', 'startDate'); + } + }; + + const recommendedCheckboxTip = ( +
+

Shows available challenges
that match your skills

+
+ ); + return (
@@ -416,7 +450,7 @@ export default function FiltersPanel({
{ - validTypes + availableTypes .map(mapTypes) .map(option => ( @@ -426,19 +460,26 @@ export default function FiltersPanel({ name={option.label} id={option.label} checked={filterState.types.includes(option.value)} - 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 }); - }} + onChange={e => handleTypeChange(option, e)} /> - + { + option.label === 'Recommended' + ? ( + + ) + + : + } )) } @@ -612,4 +653,5 @@ FiltersPanel.propTypes = { onClose: PT.func, expanded: PT.bool.isRequired, setExpanded: PT.func.isRequired, + setSort: PT.func.isRequired, }; diff --git a/src/shared/components/challenge-listing/Filters/FiltersPanel/style.scss b/src/shared/components/challenge-listing/Filters/FiltersPanel/style.scss index a3ded297f9..f658b1ebcf 100644 --- a/src/shared/components/challenge-listing/Filters/FiltersPanel/style.scss +++ b/src/shared/components/challenge-listing/Filters/FiltersPanel/style.scss @@ -285,7 +285,7 @@ display: inline-block; line-height: 30px; flex: 1 0 auto; - min-width: calc(33% - 10px); + min-width: calc(11% - 10px); &:not(:last-child) { padding-right: 10px; @@ -423,3 +423,9 @@ } } } + +.tctooltiptext { + background: $tc-gray-90; + border-radius: 6px; + padding: 10px; +} diff --git a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx index c1701786d5..86fa8dcb1b 100644 --- a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx @@ -10,7 +10,9 @@ import PT from 'prop-types'; import React, { useRef } from 'react'; // import { config } from 'topcoder-react-utils'; import Sort from 'utils/challenge-listing/sort'; -import { NO_LIVE_CHALLENGES_CONFIG, BUCKETS, BUCKET_DATA } from 'utils/challenge-listing/buckets'; +import { + NO_LIVE_CHALLENGES_CONFIG, BUCKETS, BUCKET_DATA, isRecommendedChallengeType, +} from 'utils/challenge-listing/buckets'; import SortingSelectBar from 'components/SortingSelectBar'; import Waypoint from 'react-waypoint'; // import { challenge as challengeUtils } from 'topcoder-react-lib'; @@ -51,6 +53,17 @@ export default function Bucket({ isLoggedIn, setSearchText, }) { + const activeBucketData = isRecommendedChallengeType(bucket, filterState) + ? BUCKET_DATA[bucket].sorts : BUCKET_DATA[bucket].sorts.filter(item => item !== 'updatedBy'); + + let noLiveBucket = activeBucket; + if (isRecommendedChallengeType(bucket, filterState)) { + if (isLoggedIn) { + noLiveBucket = BUCKETS.NO_RECOMMENDED_MATCH; + } else { + noLiveBucket = BUCKETS.NOT_LOGGED_IN; + } + } const refs = useRef([]); refs.current = []; const addToRefs = (el) => { @@ -116,7 +129,7 @@ export default function Bucket({ if (!loading && sortedChallenges.length === 0) { return (
- { `${NO_LIVE_CHALLENGES_CONFIG[bucket]}` } + { `${NO_LIVE_CHALLENGES_CONFIG[noLiveBucket]}` }
); } @@ -170,7 +183,7 @@ export default function Bucket({ ({ + activeBucketData.map(item => ({ label: Sort[item].name, value: item, })) diff --git a/src/shared/components/challenge-listing/Listing/index.jsx b/src/shared/components/challenge-listing/Listing/index.jsx index 73c19a7d48..5019b8ae9f 100644 --- a/src/shared/components/challenge-listing/Listing/index.jsx +++ b/src/shared/components/challenge-listing/Listing/index.jsx @@ -7,7 +7,7 @@ import React from 'react'; import PT from 'prop-types'; import { connect } from 'react-redux'; import { - BUCKETS, isReviewOpportunitiesBucket, NO_LIVE_CHALLENGES_CONFIG, + BUCKETS, isReviewOpportunitiesBucket, NO_LIVE_CHALLENGES_CONFIG, isRecommendedChallengeType, // BUCKETS, getBuckets, isReviewOpportunitiesBucket, NO_LIVE_CHALLENGES_CONFIG, } from 'utils/challenge-listing/buckets'; // import { challenge as challengeUtils } from 'topcoder-react-lib'; @@ -25,6 +25,7 @@ function Listing({ allMyChallengesLoaded, allMyPastChallengesLoaded, allChallengesLoaded, + allRecommendedChallengesLoaded, allPastChallengesLoaded, allOpenForRegistrationChallengesLoaded, challenges, @@ -32,6 +33,7 @@ function Listing({ myChallenges, myPastChallenges, allChallenges, + recommendedChallenges, pastChallenges, challengeTypes, // userChallenges, @@ -47,7 +49,9 @@ function Listing({ loadMoreMy, loadMoreMyPast, loadingAllChallenges, + loadingRecommendedChallenges, loadMoreAll, + loadMoreRecommended, loadingOpenForRegistrationChallenges, loadMoreOpenForRegistration, loadingOnGoingChallenges, @@ -119,10 +123,17 @@ function Listing({ newExpanded = newExpanded || (+meta.myPastChallengesCount === bucketChallenges.length); break; case BUCKETS.OPEN_FOR_REGISTRATION: - bucketChallenges = [].concat(openForRegistrationChallenges); - loading = loadingOpenForRegistrationChallenges; - loadMore = allOpenForRegistrationChallengesLoaded ? null : loadMoreOpenForRegistration; - newExpanded = newExpanded || (+meta.openChallengesCount === bucketChallenges.length); + if (isRecommendedChallengeType(bucket, filterState)) { + bucketChallenges = [].concat(recommendedChallenges); + loading = loadingRecommendedChallenges; + loadMore = allRecommendedChallengesLoaded ? null : loadMoreRecommended; + newExpanded = newExpanded || (+meta.allChallengesCount === bucketChallenges.length); + } else { + bucketChallenges = [].concat(openForRegistrationChallenges); + loading = loadingOpenForRegistrationChallenges; + loadMore = allOpenForRegistrationChallengesLoaded ? null : loadMoreOpenForRegistration; + newExpanded = newExpanded || (+meta.openChallengesCount === bucketChallenges.length); + } break; case BUCKETS.ONGOING: bucketChallenges = [].concat(challenges); @@ -210,6 +221,15 @@ function Listing({ ); } + let noLiveBucket = activeBucket; + if (isRecommendedChallengeType(activeBucket, filterState)) { + if (_.get(auth, 'user.userId')) { + noLiveBucket = BUCKETS.NO_RECOMMENDED_MATCH; + } else { + noLiveBucket = BUCKETS.NOT_LOGGED_IN; + } + } + // let isFilled = isChallengesAvailable(BUCKETS.OPEN_FOR_REGISTRATION) // || isChallengesAvailable(BUCKETS.ONGOING); // if (auth.user) { @@ -224,11 +244,13 @@ function Listing({ //
// ); // } + const loading = loadingMyChallenges || loadingMyPastChallenges || loadingOpenForRegistrationChallenges || loadingOnGoingChallenges || loadingAllChallenges + || loadingRecommendedChallenges || loadingPastChallenges; const placeholders = []; if (challenges.length > 0 || (activeBucket === BUCKETS.ALL && allChallenges.length > 0)) { @@ -255,7 +277,7 @@ function Listing({ loading ? placeholders : ( -
{ `${NO_LIVE_CHALLENGES_CONFIG[activeBucket]}` }
+
{ `${NO_LIVE_CHALLENGES_CONFIG[noLiveBucket]}` }
) }
@@ -268,6 +290,7 @@ Listing.defaultProps = { myChallenges: [], myPastChallenges: [], allChallenges: [], + recommendedChallenges: [], pastChallenges: [], challengeTypes: [], communityName: null, @@ -281,6 +304,7 @@ Listing.defaultProps = { loadMoreMy: null, loadMoreMyPast: null, loadMoreAll: null, + loadMoreRecommended: null, loadMoreOpenForRegistration: null, loadMoreOnGoing: null, preListingMsg: null, @@ -306,6 +330,7 @@ Listing.propTypes = { allMyChallengesLoaded: PT.bool.isRequired, allMyPastChallengesLoaded: PT.bool.isRequired, allChallengesLoaded: PT.bool.isRequired, + allRecommendedChallengesLoaded: PT.bool.isRequired, allPastChallengesLoaded: PT.bool.isRequired, allOpenForRegistrationChallengesLoaded: PT.bool.isRequired, challenges: PT.arrayOf(PT.shape()), @@ -313,6 +338,7 @@ Listing.propTypes = { myChallenges: PT.arrayOf(PT.shape()), myPastChallenges: PT.arrayOf(PT.shape()), allChallenges: PT.arrayOf(PT.shape()), + recommendedChallenges: PT.arrayOf(PT.shape()), pastChallenges: PT.arrayOf(PT.shape()), challengeTypes: PT.arrayOf(PT.shape()), challengesUrl: PT.string.isRequired, @@ -326,12 +352,14 @@ Listing.propTypes = { loadingMyChallenges: PT.bool.isRequired, loadingMyPastChallenges: PT.bool.isRequired, loadingAllChallenges: PT.bool.isRequired, + loadingRecommendedChallenges: PT.bool.isRequired, loadingOpenForRegistrationChallenges: PT.bool.isRequired, loadingOnGoingChallenges: PT.bool.isRequired, loadingReviewOpportunities: PT.bool.isRequired, loadMoreMy: PT.func, loadMoreMyPast: PT.func, loadMoreAll: PT.func, + loadMoreRecommended: PT.func, loadMoreOnGoing: PT.func, loadMoreOpenForRegistration: PT.func, loadMorePast: PT.func, @@ -362,6 +390,8 @@ const mapStateToProps = (state) => { allMyChallengesLoaded: cl.allMyChallengesLoaded, allMyPastChallengesLoaded: cl.allMyPastChallengesLoaded, allChallengesLoaded: cl.allChallengesLoaded, + allRecommendedChallengesLoaded: cl.allRecommendedChallengesLoaded, + recommendedChallenges: cl.recommendedChallenges, allPastChallengesLoaded: cl.allPastChallengesLoaded, allOpenForRegistrationChallengesLoaded: cl.allOpenForRegistrationChallengesLoaded, // pastSearchTimestamp: cl.pastSearchTimestamp, diff --git a/src/shared/components/challenge-listing/index.jsx b/src/shared/components/challenge-listing/index.jsx index 87b2e12176..b0e97abe6b 100644 --- a/src/shared/components/challenge-listing/index.jsx +++ b/src/shared/components/challenge-listing/index.jsx @@ -118,12 +118,14 @@ export default function ChallengeListing(props) { loadingMyChallenges={props.loadingMyChallenges} loadingMyPastChallenges={props.loadingMyPastChallenges} loadingAllChallenges={props.loadingAllChallenges} + loadingRecommendedChallenges={props.loadingRecommendedChallenges} loadingOpenForRegistrationChallenges={props.loadingOpenForRegistrationChallenges} loadingOnGoingChallenges={props.loadingOnGoingChallenges} loadingReviewOpportunities={props.loadingReviewOpportunities} loadMoreMy={props.loadMoreMy} loadMoreMyPast={props.loadMoreMyPast} loadMoreAll={props.loadMoreAll} + loadMoreRecommended={props.loadMoreRecommended} loadMoreOpenForRegistration={props.loadMoreOpenForRegistration} loadMoreOnGoing={props.loadMoreOnGoing} loadMorePast={props.loadMorePast} @@ -190,6 +192,7 @@ ChallengeListing.defaultProps = { loadMoreMy: null, loadMoreMyPast: null, loadMoreAll: null, + loadMoreRecommended: null, loadMoreOpenForRegistration: null, loadMoreOnGoing: null, loadMorePast: null, @@ -232,6 +235,7 @@ ChallengeListing.propTypes = { loadingMyChallenges: PT.bool.isRequired, loadingMyPastChallenges: PT.bool.isRequired, loadingAllChallenges: PT.bool.isRequired, + loadingRecommendedChallenges: PT.bool.isRequired, loadingOpenForRegistrationChallenges: PT.bool.isRequired, loadingOnGoingChallenges: PT.bool.isRequired, loadingPastChallenges: PT.bool.isRequired, @@ -239,6 +243,7 @@ ChallengeListing.propTypes = { loadMoreMy: PT.func, loadMoreMyPast: PT.func, loadMoreAll: PT.func, + loadMoreRecommended: PT.func, loadMoreOpenForRegistration: PT.func, loadMoreOnGoing: PT.func, loadMorePast: PT.func, diff --git a/src/shared/containers/challenge-listing/FilterPanel.jsx b/src/shared/containers/challenge-listing/FilterPanel.jsx index 43ce2ff7da..1a05dbbc9d 100644 --- a/src/shared/containers/challenge-listing/FilterPanel.jsx +++ b/src/shared/containers/challenge-listing/FilterPanel.jsx @@ -102,7 +102,7 @@ export class Container extends React.Component { if (!filterState.types.length && validTypes.length && !this.initialDefaultChallengeTypes) { setFilterState({ ..._.clone(filterState), - types: validTypes.map(item => item.abbreviation), + types: validTypes.map(item => item.abbreviation).filter(item => item !== 'REC'), }); this.initialDefaultChallengeTypes = true; } @@ -121,6 +121,7 @@ export class Container extends React.Component { setExpanded, hidden, onClose, + setSort, } = this.props; const communityFilters2 = [ { @@ -146,6 +147,7 @@ export class Container extends React.Component { setExpanded={setExpanded} hidden={hidden} onClose={onClose} + setSort={setSort} /> ); @@ -190,6 +192,7 @@ Container.propTypes = { onClose: PT.func.isRequired, validTypes: PT.arrayOf(PT.shape()).isRequired, setSearchText: PT.func.isRequired, + setSort: PT.func.isRequired, }; function mapDispatchToProps(dispatch) { @@ -213,6 +216,7 @@ function mapDispatchToProps(dispatch) { selectCommunity: id => dispatch(cla.selectCommunity(id)), setFilterState: s => dispatch(cla.setFilter(s)), onClose: () => dispatch(a.setExpanded(false)), + setSort: (bucket, sort) => dispatch(cla.setSort(bucket, sort)), }; } diff --git a/src/shared/containers/challenge-listing/Listing/index.jsx b/src/shared/containers/challenge-listing/Listing/index.jsx index 8c996bd8e8..0bd812b013 100644 --- a/src/shared/containers/challenge-listing/Listing/index.jsx +++ b/src/shared/containers/challenge-listing/Listing/index.jsx @@ -24,7 +24,9 @@ import sidebarActions from 'actions/challenge-listing/sidebar'; import filterPanelActions from 'actions/challenge-listing/filter-panel'; import communityActions from 'actions/tc-communities'; // import SORT from 'utils/challenge-listing/sort'; -import { BUCKETS, filterChanged, sortChangedBucket } from 'utils/challenge-listing/buckets'; +import { + BUCKETS, filterChanged, sortChangedBucket, isRecommendedChallengeType, +} from 'utils/challenge-listing/buckets'; import MetaTags from 'components/MetaTags'; import { USER_GROUP_MAXAGE } from 'config'; import { updateChallengeType } from 'utils/challenge'; @@ -107,8 +109,10 @@ export class ListingContainer extends React.Component { dropMyPastChallenges, getMyPastChallenges, dropAllChallenges, + dropRecommendedChallenges, getAllChallenges, getOpenForRegistrationChallenges, + getRecommendedChallenges, getActiveChallenges, dropActiveChallenges, dropOpenForRegistrationChallenges, @@ -164,13 +168,23 @@ export class ListingContainer extends React.Component { break; } case BUCKETS.OPEN_FOR_REGISTRATION: { - dropOpenForRegistrationChallenges(); - getOpenForRegistrationChallenges( - 0, - fA.back, - auth.tokenV3, - fA.front, - ); + if (isRecommendedChallengeType(bucket, filter)) { + dropOpenForRegistrationChallenges(); + getRecommendedChallenges( + 0, + sorts, + auth.tokenV3, + filter, + ); + } else { + dropRecommendedChallenges(); + getOpenForRegistrationChallenges( + 0, + fA.back, + auth.tokenV3, + fA.front, + ); + } break; } case BUCKETS.ONGOING: { @@ -220,7 +234,11 @@ export class ListingContainer extends React.Component { return; } if (filterChanged(filter, prevProps.filter)) { - this.reloadChallenges(); + if (isRecommendedChallengeType(bucket, prevProps.filter)) { + this.reloadRecommendedChallenges(); + } else { + this.reloadChallenges(); + } } setTimeout(() => { selectBucketDone(); @@ -274,12 +292,32 @@ export class ListingContainer extends React.Component { }; } + reloadRecommendedChallenges() { + const { + sorts, + auth, + filter, + dropOpenForRegistrationChallenges, + getRecommendedChallenges, + } = this.props; + + dropOpenForRegistrationChallenges(); + getRecommendedChallenges( + 0, + sorts, + auth.tokenV3, + filter, + ); + } + loadChallenges() { const { auth, // DISABLED: Until api receive fix community-app#5073 // getActiveChallenges, getOpenForRegistrationChallenges, + getRecommendedChallenges, + allRecommendedChallengesLoaded, getMyChallenges, getMyPastChallenges, getAllChallenges, @@ -328,6 +366,14 @@ export class ListingContainer extends React.Component { auth.tokenV3, f.front, ); + if (!allRecommendedChallengesLoaded) { + getRecommendedChallenges( + 0, + f.back, + auth.tokenV3, + f.front, + ); + } } getPastChallenges( 0, @@ -420,6 +466,7 @@ export class ListingContainer extends React.Component { lastRequestedPageOfMyChallenges, lastRequestedPageOfMyPastChallenges, lastRequestedPageOfAllChallenges, + lastRequestedPageOfRecommendedChallenges, lastRequestedPageOfActiveChallenges, lastRequestedPageOfOpenForRegistrationChallenges, lastRequestedPageOfPastChallenges, @@ -430,6 +477,7 @@ export class ListingContainer extends React.Component { loadingMyChallengesUUID, loadingMyPastChallengesUUID, loadingAllChallengesUUID, + loadingRecommendedChallengesUUID, loadingPastChallengesUUID, loadingReviewOpportunitiesUUID, listingOnly, @@ -515,6 +563,16 @@ export class ListingContainer extends React.Component { ); }; + const loadMoreRecommended = () => { + const f = this.getBackendFilter(); + getAllChallenges( + 1 + lastRequestedPageOfRecommendedChallenges, + f.back, + tokenV3, + f.front, + ); + }; + let loadMoreReviewOpportunities; if (!allReviewOpportunitiesLoaded) { loadMoreReviewOpportunities = () => getReviewOpportunities( @@ -582,6 +640,7 @@ export class ListingContainer extends React.Component { loadingMyChallenges={Boolean(loadingMyChallengesUUID)} loadingMyPastChallenges={Boolean(loadingMyPastChallengesUUID)} loadingAllChallenges={Boolean(loadingAllChallengesUUID)} + loadingRecommendedChallenges={Boolean(loadingRecommendedChallengesUUID)} loadingOpenForRegistrationChallenges={Boolean(loadingOpenForRegistrationChallengesUUID)} loadingOnGoingChallenges={Boolean(loadingActiveChallengesUUID)} // eslint-disable-next-line max-len @@ -600,6 +659,7 @@ export class ListingContainer extends React.Component { loadMoreMy={loadMoreMy} loadMoreMyPast={loadMoreMyPast} loadMoreAll={loadMoreAll} + loadMoreRecommended={loadMoreRecommended} loadMoreOpenForRegistration={loadMoreOpenForRegistration} loadMoreOnGoing={loadMoreOnGoing} reviewOpportunities={reviewOpportunities} @@ -660,6 +720,7 @@ ListingContainer.propTypes = { // allActiveChallengesLoaded: PT.bool.isRequired, // allPastChallengesLoaded: PT.bool.isRequired, allReviewOpportunitiesLoaded: PT.bool.isRequired, + allRecommendedChallengesLoaded: PT.bool.isRequired, ChallengeListingBanner: PT.node, challenges: PT.arrayOf(PT.shape({})).isRequired, // active challenges. openForRegistrationChallenges: PT.arrayOf(PT.shape({})).isRequired, @@ -683,6 +744,7 @@ ListingContainer.propTypes = { dropMyChallenges: PT.func.isRequired, dropMyPastChallenges: PT.func.isRequired, dropAllChallenges: PT.func.isRequired, + dropRecommendedChallenges: PT.func.isRequired, dropOpenForRegistrationChallenges: PT.func.isRequired, dropActiveChallenges: PT.func.isRequired, dropPastChallenges: PT.func.isRequired, @@ -695,6 +757,7 @@ ListingContainer.propTypes = { // extraBucket: PT.string, getActiveChallenges: PT.func.isRequired, getOpenForRegistrationChallenges: PT.func.isRequired, + getRecommendedChallenges: PT.func.isRequired, getMyChallenges: PT.func.isRequired, getMyPastChallenges: PT.func.isRequired, getAllChallenges: PT.func.isRequired, @@ -708,6 +771,7 @@ ListingContainer.propTypes = { lastRequestedPageOfMyChallenges: PT.number.isRequired, lastRequestedPageOfMyPastChallenges: PT.number.isRequired, lastRequestedPageOfAllChallenges: PT.number.isRequired, + lastRequestedPageOfRecommendedChallenges: PT.number.isRequired, lastRequestedPageOfPastChallenges: PT.number.isRequired, lastRequestedPageOfReviewOpportunities: PT.number.isRequired, // lastUpdateOfActiveChallenges: PT.number.isRequired, @@ -716,6 +780,7 @@ ListingContainer.propTypes = { loadingMyChallengesUUID: PT.string.isRequired, loadingMyPastChallengesUUID: PT.string.isRequired, loadingAllChallengesUUID: PT.string.isRequired, + loadingRecommendedChallengesUUID: PT.string.isRequired, loadingPastChallengesUUID: PT.string.isRequired, loadingReviewOpportunitiesUUID: PT.string.isRequired, markHeaderMenu: PT.func.isRequired, @@ -757,6 +822,7 @@ const mapStateToProps = (state, ownProps) => { auth: state.auth, // allActiveChallengesLoaded: cl.allActiveChallengesLoaded, allPastChallengesLoaded: cl.allPastChallengesLoaded, + allRecommendedChallengesLoaded: cl.allRecommendedChallengesLoaded, allReviewOpportunitiesLoaded: cl.allReviewOpportunitiesLoaded, filter: cl.filter, challenges: cl.challenges, @@ -779,6 +845,7 @@ const mapStateToProps = (state, ownProps) => { lastRequestedPageOfMyChallenges: cl.lastRequestedPageOfMyChallenges, lastRequestedPageOfMyPastChallenges: cl.lastRequestedPageOfMyPastChallenges, lastRequestedPageOfAllChallenges: cl.lastRequestedPageOfAllChallenges, + lastRequestedPageOfRecommendedChallenges: cl.lastRequestedPageOfRecommendedChallenges, lastRequestedPageOfPastChallenges: cl.lastRequestedPageOfPastChallenges, lastRequestedPageOfReviewOpportunities: cl.lastRequestedPageOfReviewOpportunities, // lastUpdateOfActiveChallenges: cl.lastUpdateOfActiveChallenges, @@ -787,6 +854,7 @@ const mapStateToProps = (state, ownProps) => { loadingMyChallengesUUID: cl.loadingMyChallengesUUID, loadingMyPastChallengesUUID: cl.loadingMyPastChallengesUUID, loadingAllChallengesUUID: cl.loadingAllChallengesUUID, + loadingRecommendedChallengesUUID: cl.loadingRecommendedChallengesUUID, loadingPastChallengesUUID: cl.loadingPastChallengesUUID, loadingReviewOpportunitiesUUID: cl.loadingReviewOpportunitiesUUID, loadingChallengeTypes: cl.loadingChallengeTypes, @@ -845,11 +913,17 @@ function mapDispatchToProps(dispatch) { dispatch(a.getAllChallengesDone(uuid, page, filter, token, frontFilter)); }, dropAllChallenges: () => dispatch(a.dropAllChallenges()), + dropRecommendedChallenges: () => dispatch(a.dropRecommendedChallenges()), getTotalChallengesCount: (token, frontFilter) => { const uuid = shortId(); dispatch(a.getTotalChallengesCountInit(uuid)); dispatch(a.getTotalChallengesCountDone(uuid, token, frontFilter)); }, + getRecommendedChallenges: (page, sort, token, filter) => { + const uuid = shortId(); + dispatch(a.getRecommendedChallengesInit(uuid, page, sort)); + dispatch(a.getRecommendedChallengesDone(uuid, token, sort, filter)); + }, // getRestActiveChallenges: (token, filter) => { // const uuid = shortId(); // dispatch(a.getRestActiveChallengesInit(uuid)); diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js index 41ecc6b941..4f589b4e12 100644 --- a/src/shared/reducers/challenge-listing/index.js +++ b/src/shared/reducers/challenge-listing/index.js @@ -149,6 +149,15 @@ function onGetAllChallengesInit(state, { payload }) { }; } +function onGetRecommendedChallengesInit(state, { payload }) { + return { + ...state, + loadingRecommendedChallengesUUID: payload.uuid, + lastRequestedPageOfRecommendedChallenges: payload.page, + recommendedChallenges: [], + }; +} + function onGetMyPastChallengesInit(state, { payload }) { return { ...state, @@ -636,6 +645,26 @@ function onGetAllChallengesDone(state, { error, payload }) { }; } +function onGetRecommendedChallengesDone(state, { error, payload }) { + if (error) { + logger.error(payload); + return state; + } + const { uuid, recommendedChallenges: loaded } = payload; + if (uuid !== state.loadingRecommendedChallengesUUID) return state; + const challenges = loaded; + return { + ...state, + recommendedChallenges: challenges, + loadingRecommendedChallengesUUID: '', + allRecommendedChallengesLoaded: challenges.length >= payload.meta.allRecommendedChallengesCount, + meta: { + ...state.meta, + allRecommendedChallengesCount: payload.meta.allRecommendedChallengesCount, + }, + }; +} + function onGetMyPastChallengesDone(state, { error, payload }) { if (error) { logger.error(payload); @@ -694,6 +723,7 @@ function create(initialState) { allActiveChallengesLoaded: false, allMyChallengesLoaded: false, allChallengesLoaded: false, + allRecommendedChallengesLoaded: false, allOpenForRegistrationChallengesLoaded: false, allPastChallengesLoaded: false, // allReviewOpportunitiesLoaded: false, @@ -708,6 +738,7 @@ function create(initialState) { lastRequestedPageOfMyChallenges: -1, lastRequestedPageOfMyPastChallenges: -1, lastRequestedPageOfAllChallenges: -1, + lastRequestedPageOfRecommendedChallenges: -1, lastRequestedPageOfPastChallenges: -1, // lastRequestedPageOfReviewOpportunities: -1, // lastUpdateOfActiveChallenges: 0, @@ -773,6 +804,12 @@ function create(initialState) { lastRequestedPageOfAllChallenges: -1, loadingAllChallengesUUID: '', }), + [a.dropRecommendedChallenges]: state => ({ + ...state, + allChallenges: [], + lastRequestedPageOfRecommendedChallenges: -1, + loadingAllChallengesUUID: '', + }), [a.dropPastChallenges]: state => ({ ...state, pastChallenges: [], @@ -811,6 +848,9 @@ function create(initialState) { [a.getAllChallengesInit]: onGetAllChallengesInit, [a.getAllChallengesDone]: onGetAllChallengesDone, + [a.getRecommendedChallengesInit]: onGetRecommendedChallengesInit, + [a.getRecommendedChallengesDone]: onGetRecommendedChallengesDone, + [a.getTotalChallengesCountInit]: onGetTotalChallengesCountInit, [a.getTotalChallengesCountDone]: onGetTotalChallengesCountDone, @@ -857,6 +897,7 @@ function create(initialState) { allMyPastChallengesLoaded: false, allOpenForRegistrationChallengesLoaded: false, allChallengesLoaded: false, + allRecommendedChallengesLoaded: false, allPastChallengesLoaded: false, allReviewOpportunitiesLoaded: false, @@ -866,7 +907,7 @@ function create(initialState) { openForRegistrationChallenges: [], pastChallenges: [], myPastChallenges: [], - recommendedChallenges: {}, + recommendedChallenges: [], challengeTypes: [], challengeTypesMap: {}, challengeTags: [], @@ -879,6 +920,7 @@ function create(initialState) { lastRequestedPageOfOpenForRegistrationChallenges: -1, lastRequestedPageOfMyChallenges: -1, lastRequestedPageOfAllChallenges: -1, + lastRequestedPageOfRecommendedChallenges: -1, lastRequestedPageOfMyPastChallenges: -1, lastRequestedPageOfPastChallenges: -1, lastRequestedPageOfReviewOpportunities: -1, @@ -938,6 +980,7 @@ function create(initialState) { meta: { allChallengesCount: 0, + allRecommendedChallengesCount: 0, myChallengesCount: 0, ongoingChallengesCount: 0, openChallengesCount: 0, diff --git a/src/shared/utils/challenge-listing/buckets.js b/src/shared/utils/challenge-listing/buckets.js index e50a724de3..0e83256b57 100644 --- a/src/shared/utils/challenge-listing/buckets.js +++ b/src/shared/utils/challenge-listing/buckets.js @@ -18,6 +18,8 @@ export const BUCKETS = { // SAVED_REVIEW_OPPORTUNITIES_FILTER: 'savedReviewOpportunitiesFilter', ALL_PAST: 'allPast', MY_PAST: 'myPast', + NOT_LOGGED_IN: 'bestMatchNotLoggedIn', + NO_RECOMMENDED_MATCH: 'noRecommendedMatch', }; export const BUCKET_DATA = { @@ -58,6 +60,7 @@ export const BUCKET_DATA = { // hideCount: false, name: 'Open for registration', sorts: [ + SORTS.BEST_MATCH, SORTS.MOST_RECENT_START_DATE, // SORTS.TIME_TO_REGISTER, // SORTS.TIME_TO_SUBMIT, @@ -149,6 +152,8 @@ export const NO_LIVE_CHALLENGES_CONFIG = { // [BUCKETS.UPCOMING]: 'No challenges found in Upcoming Challenges', [BUCKETS.ALL_PAST]: 'No challenges found in All Past Challenges', [BUCKETS.MY_PAST]: 'No challenges found in My Past Challenges', + [BUCKETS.NOT_LOGGED_IN]: 'There are no recommended challenges for you. This could be because you are not logged in and/or the recommendation tool will only recommend challenges that match your skills once you successfully place in a challenge. Try exploring other challenges or checking back later.', + [BUCKETS.NO_RECOMMENDED_MATCH]: 'There are no challenges open for registration at the moment that match your skills. Try exploring other challenges or checking back later.', }; /** @@ -264,4 +269,13 @@ export function isPastBucket(bucket) { return [BUCKETS.ALL_PAST, BUCKETS.MY_PAST].indexOf(bucket) !== -1; } +/** + * Checks if current challenge type is recommended challenge type + * @param {String} bucket bucket name + * @param {Object} filterState current filter state +*/ +export function isRecommendedChallengeType(bucket, filterState) { + return bucket === 'openForRegistration' && filterState.types.length === 1 && filterState.types[0] === 'REC'; +} + export default undefined; diff --git a/src/shared/utils/challenge-listing/sort.js b/src/shared/utils/challenge-listing/sort.js index ce537b2ac5..3ac75f6cfa 100644 --- a/src/shared/utils/challenge-listing/sort.js +++ b/src/shared/utils/challenge-listing/sort.js @@ -19,6 +19,7 @@ export const SORTS = { REVIEW_OPPORTUNITIES_TITLE_A_TO_Z: 'review-opportunities-title-a-to-z', REVIEW_OPPORTUNITIES_PAYMENT: 'review-opportunities-payment', REVIEW_OPPORTUNITIES_START_DATE: 'review-opportunities-start-date', + BEST_MATCH: 'updatedBy', }; export default { @@ -98,4 +99,9 @@ export default { func: (a, b) => moment(a.startDate) - moment(b.startDate), name: 'Review start date', }, + [SORTS.BEST_MATCH]: { + func: (a, b) => parseFloat(a.matchScore) - parseFloat(b.matchScore), + name: 'Best Match', + order: 'asc', + }, }; From efc8e1d70e95cbe0a2cafcf169c86819b0f93707 Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues Date: Wed, 27 Jan 2021 04:33:28 -0300 Subject: [PATCH 02/36] ci: Deploy feature/poc-recommender-sub-2 to QA env --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d723a4d47b..f1d6e65d41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -289,7 +289,7 @@ workflows: filters: branches: only: - - free + - feature/poc-recommender-sub-2 # This is beta env for production soft releases - "build-prod-beta": context : org-global From 295ce041aa19b0e419f50679ece7f44ba4d779e9 Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues Date: Wed, 27 Jan 2021 05:00:09 -0300 Subject: [PATCH 03/36] Test Release - POC Recommender Submission 2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a27f2f045d..4a25c39a33 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "supertest": "^3.1.0", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", "tc-ui": "^1.0.12", - "topcoder-react-lib": "1.1.6", + "topcoder-react-lib": "1000.27.1", "topcoder-react-ui-kit": "2.0.1", "topcoder-react-utils": "0.7.8", "turndown": "^4.0.2", From ef322021ce45e0c4425820d2d825b9cd4537f5ff Mon Sep 17 00:00:00 2001 From: nursoltan-s Date: Thu, 28 Jan 2021 18:24:31 +0800 Subject: [PATCH 04/36] final fixes --- .../__snapshots__/FiltersPanel.jsx.snap | 1 + .../ChallengeCard/MatchScore/index.jsx | 20 ++++ .../ChallengeCard/MatchScore/style.scss | 12 +++ .../challenge-listing/ChallengeCard/index.jsx | 22 +++-- .../ChallengeCard/style.scss | 4 + .../Filters/FiltersPanel/index.jsx | 96 ++++++++++++------- .../Filters/FiltersPanel/style.scss | 12 ++- .../reducers/challenge-listing/index.js | 2 +- src/shared/utils/challenge-listing/buckets.js | 2 +- src/shared/utils/challenge-listing/helper.js | 8 ++ src/styles/awesome.css | 4 + 11 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx create mode 100644 src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss 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..e59c87c240 100644 --- a/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap +++ b/__tests__/shared/components/challenge-listing/Filters/__snapshots__/FiltersPanel.jsx.snap @@ -184,6 +184,7 @@ exports[`Matches shallow shapshot 2`] = `
+
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..4e7296408d --- /dev/null +++ b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/index.jsx @@ -0,0 +1,20 @@ +import PT from 'prop-types'; +import React from 'react'; + +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..563eebd007 --- /dev/null +++ b/src/shared/components/challenge-listing/ChallengeCard/MatchScore/style.scss @@ -0,0 +1,12 @@ +@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; +} diff --git a/src/shared/components/challenge-listing/ChallengeCard/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/index.jsx index ab1dec84e4..ac8c1941cd 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 @@ -67,13 +69,19 @@ 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.matchScore + && + } +
{challenge.status === 'Active' ? 'Ends ' : 'Ended '} diff --git a/src/shared/components/challenge-listing/ChallengeCard/style.scss b/src/shared/components/challenge-listing/ChallengeCard/style.scss index 31bb19c0ed..d8277e6c0e 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/style.scss +++ b/src/shared/components/challenge-listing/ChallengeCard/style.scss @@ -106,6 +106,10 @@ $challenge-radius-4: $corner-radius * 2; display: inline-block; } } + + .challenge-detail-header { + display: flex; + } } // date and technologies .details-footer { diff --git a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx index 4771035e5d..2b097830b7 100644 --- a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx +++ b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx @@ -22,7 +22,7 @@ /* eslint-disable jsx-a11y/label-has-for */ import _ from 'lodash'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import PT from 'prop-types'; import Select from 'components/Select'; import DateRangePicker from 'components/DateRangePicker'; @@ -250,8 +250,30 @@ export default function FiltersPanel({ const past = isPastBucket(activeBucket); const disableClearFilterButtons = isFilterEmpty(filterState, past ? 'past' : '', activeBucket); - const availableTypes = activeBucket === 'openForRegistration' - ? validTypes : validTypes.filter(item => item.abbreviation !== 'REC'); + const availableTypes = validTypes.filter(item => item.abbreviation !== 'REC'); + const isRecommendedChallengesVisible = activeBucket === 'openForRegistration'; + const [recommendedToggle, setRecommendedToggle] = useState(false); + + useEffect(() => { + if (recommendedToggle) { + const types = _.union(filterState.types, ['REC']); + setFilterState({ ..._.clone(filterState), types }); + } + }, []); + + const onSwitchRecommendedChallenge = (on) => { + const { types } = filterState; + types.push('REC'); + setRecommendedToggle(on); + + if (on) { + setSort('openForRegistration', 'updatedBy'); + setFilterState({ ..._.clone(filterState), types }); + } else { + setFilterState({ ..._.clone(filterState), types: ['TSK', 'CH', 'F2F'] }); + setSort('openForRegistration', 'startDate'); + } + }; const handleTypeChange = (option, e) => { let { types } = filterState; @@ -261,20 +283,14 @@ export default function FiltersPanel({ types = types.filter(type => type !== option.value); } - if (option.label === 'Recommended') { - types = types.filter(type => type === 'REC'); - if (!e.target.checked) { - setFilterState({ ..._.clone(filterState), types: ['TSK', 'CH', 'F2F'] }); - setSort('openForRegistration', 'startDate'); - } else { - setSort('openForRegistration', 'updatedBy'); - setFilterState({ ..._.clone(filterState), types }); - } + if (recommendedToggle) { + types = [...types, 'REC']; } else { types = types.filter(type => type !== 'REC'); - setFilterState({ ..._.clone(filterState), types }); - setSort('openForRegistration', 'startDate'); } + + setFilterState({ ..._.clone(filterState), types }); + setSort('openForRegistration', 'startDate'); }; const recommendedCheckboxTip = ( @@ -462,24 +478,7 @@ export default function FiltersPanel({ checked={filterState.types.includes(option.value)} onChange={e => handleTypeChange(option, e)} /> - { - option.label === 'Recommended' - ? ( - - ) - - : - } + )) } @@ -579,8 +578,41 @@ export default function FiltersPanel({
) } + + { + isRecommendedChallengesVisible + && ( +
+ + + + +
+ +