From f33f20fa1e9562582619f3c76470cb5401eeb8a6 Mon Sep 17 00:00:00 2001 From: gurmeetb Date: Sat, 27 Apr 2019 15:36:13 +0530 Subject: [PATCH 1/4] fix for issue #2082 --- src/shared/components/Settings/Profile/Hobby/index.jsx | 2 ++ src/shared/components/Settings/Profile/Hobby/styles.scss | 6 ++++++ src/shared/components/Settings/Profile/Skills/index.jsx | 1 + src/shared/components/Settings/Profile/Skills/styles.scss | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/src/shared/components/Settings/Profile/Hobby/index.jsx b/src/shared/components/Settings/Profile/Hobby/index.jsx index 6fc926ea26..b5ded45959 100644 --- a/src/shared/components/Settings/Profile/Hobby/index.jsx +++ b/src/shared/components/Settings/Profile/Hobby/index.jsx @@ -341,6 +341,7 @@ export default class Hobby extends ConsentComponent {
@@ -351,6 +352,7 @@ export default class Hobby extends ConsentComponent {
@@ -349,7 +352,7 @@ Date range theme={{ button: style.button }} themePriority={PRIORITY.ADHOC_DEFAULT_CONTEXT} > -Clear filters + Clear filters -Save filter + Save filter @@ -395,4 +398,5 @@ FiltersPanel.propTypes = { validKeywords: PT.arrayOf(PT.string).isRequired, validSubtracks: PT.arrayOf(PT.shape()).isRequired, onClose: PT.func, + setDatepickerStatus: PT.func.isRequired, }; diff --git a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx index 23bf748f1b..99e07b9d57 100644 --- a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx @@ -8,17 +8,16 @@ import _ from 'lodash'; import PT from 'prop-types'; import qs from 'qs'; import React from 'react'; -import Sort from 'utils/challenge-listing/sort'; import SortingSelectBar from 'components/SortingSelectBar'; import Waypoint from 'react-waypoint'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import CardPlaceholder from '../../placeholders/ChallengeCard'; import ChallengeCard from '../../ChallengeCard'; import './style.scss'; const COLLAPSED_SIZE = 10; -const Filter = challengeUtils.filter; +const { SORTS_DATA } = challengeUtil.sort; export default function Bucket({ bucket, @@ -42,13 +41,10 @@ export default function Bucket({ userHandle, expandedTags, expandTag, + loadMoreChallenges, }) { - const filter = Filter.getFilterFunction(bucket.filter); const activeSort = sort || bucket.sorts[0]; - const sortedChallenges = _.clone(challenges); - sortedChallenges.sort(Sort[activeSort].func); - const bucketQuery = qs.stringify({ bucket: bucketId, communityId: selectedCommunityId || undefined, @@ -56,20 +52,18 @@ export default function Bucket({ }, { encodeValuesOnly: true }); let expandable = false; - const filteredChallenges = []; - for (let i = 0; i < sortedChallenges.length; i += 1) { - if (filter(sortedChallenges[i])) { - filteredChallenges.push(sortedChallenges[i]); - } - if (!expanded && filteredChallenges.length >= COLLAPSED_SIZE) { + const challengeList = []; + for (let i = 0; i < challenges.length; i += 1) { + challengeList.push(challenges[i]); + if (!expanded && challengeList.length >= COLLAPSED_SIZE) { expandable = true; break; } } - if (!filteredChallenges.length && !loadMore) return null; + if (!challengeList.length && !loadMore) return null; - const cards = filteredChallenges.map(item => ( + const cards = challengeList.map(item => ( ({ - label: Sort[item].name, + label: SORTS_DATA[item].name, value: item, })) } title={bucket.name} value={{ - label: Sort[activeSort].name, + label: SORTS_DATA[activeSort].name, value: activeSort, }} /> @@ -123,13 +117,14 @@ export default function Bucket({ expand(); document.body.scrollTop = 0; document.documentElement.scrollTop = 0; + loadMoreChallenges(bucketId); event.preventDefault(); }} role="button" styleName="view-more" tabIndex={0} > -View more challenges + View more challenges ) : null } @@ -149,6 +144,7 @@ Bucket.defaultProps = { userHandle: '', expandedTags: [], expandTag: null, + loadMoreChallenges: null, }; Bucket.propTypes = { @@ -173,4 +169,5 @@ Bucket.propTypes = { userHandle: PT.string, expandedTags: PT.arrayOf(PT.number), expandTag: PT.func, + loadMoreChallenges: PT.func, }; diff --git a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx index 87cb5eef0a..43243f35af 100644 --- a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx @@ -1,10 +1,8 @@ /** * The bucket for review opportunities. */ -import _ from 'lodash'; import PT from 'prop-types'; import React from 'react'; -import Sort from 'utils/challenge-listing/sort'; import SortingSelectBar from 'components/SortingSelectBar'; import Waypoint from 'react-waypoint'; import { challenge as challengeUtils } from 'topcoder-react-lib'; @@ -13,7 +11,7 @@ import ReviewOpportunityCard from '../../ReviewOpportunityCard'; import './style.scss'; -const Filter = challengeUtils.filter; +const { SORTS_DATA } = challengeUtils.sort; const NO_RESULTS_MESSAGE = 'There are no review opportunities available'; @@ -23,7 +21,6 @@ export default function ReviewOpportunityBucket({ challengesUrl, expandedTags, expandTag, - filterState, keepPlaceholders, loading, loadMore, @@ -32,25 +29,10 @@ export default function ReviewOpportunityBucket({ setSort, sort, }) { - if (!opportunities.length && !loadMore) return null; - const activeSort = sort || bucket.sorts[0]; - const sortedOpportunities = _.clone(opportunities); - sortedOpportunities.sort(Sort[activeSort].func); - - /* Filtering for Review Opportunities will be done entirely in the front-end - * which means it can be done at render, rather than in the reducer, - * which avoids reloading the review opportunities from server every time - * a filter is changed. */ - const filteredOpportunities = sortedOpportunities.filter( - Filter.getReviewOpportunitiesFilterFunction({ - ...bucket.filter, // Default bucket filters from utils/buckets.js - ...filterState, // User selected filters - }), - ); - const cards = filteredOpportunities.map(item => ( + const cards = opportunities.map(item => ( ({ - label: Sort[item].name, + label: SORTS_DATA[item].name, value: item, })) } value={{ - label: Sort[activeSort].name, + label: SORTS_DATA[activeSort].name, value: activeSort, }} /> {cards} { - !loading && filteredOpportunities.length === 0 && ( + !loading && opportunities.length === 0 && (
{NO_RESULTS_MESSAGE}
@@ -118,7 +100,6 @@ ReviewOpportunityBucket.propTypes = { challengesUrl: PT.string.isRequired, expandedTags: PT.arrayOf(PT.number), expandTag: PT.func, - filterState: PT.shape().isRequired, opportunities: PT.arrayOf(PT.shape()).isRequired, keepPlaceholders: PT.bool, loading: PT.bool, diff --git a/src/shared/components/challenge-listing/Listing/index.jsx b/src/shared/components/challenge-listing/Listing/index.jsx index 59c5273960..9a167017ef 100644 --- a/src/shared/components/challenge-listing/Listing/index.jsx +++ b/src/shared/components/challenge-listing/Listing/index.jsx @@ -6,11 +6,13 @@ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; import { connect } from 'react-redux'; -import { BUCKETS, getBuckets, isReviewOpportunitiesBucket } from 'utils/challenge-listing/buckets'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import Bucket from './Bucket'; import ReviewOpportunityBucket from './ReviewOpportunityBucket'; import './style.scss'; +const { BUCKETS, getBuckets, isReviewOpportunitiesBucket } = challengeUtil.buckets; + function Listing({ activeBucket, auth, @@ -21,8 +23,14 @@ function Listing({ filterState, keepPastPlaceholders, loadingPastChallenges, + loadingMyChallenges, + loadingOnGoingChallenges, + loadingOpenChallenges, loadingReviewOpportunities, loadMorePast, + loadMoreMy, + loadMoreOpen, + loadMoreOnGoing, loadMoreReviewOpportunities, newChallengeDetails, openChallengesInNewTabs, @@ -37,6 +45,7 @@ function Listing({ sorts, expandedTags, expandTag, + loadMoreChallenges, }) { const buckets = getBuckets(_.get(auth.user, 'handle')); const getBucket = (bucket, expanded = false) => { @@ -49,6 +58,18 @@ function Listing({ loading = loadingPastChallenges; loadMore = loadMorePast; break; + case BUCKETS.MY: + loading = loadingMyChallenges; + loadMore = loadMoreMy; + break; + case BUCKETS.ONGOING: + loading = loadingOnGoingChallenges; + loadMore = loadMoreOnGoing; + break; + case BUCKETS.OPEN_FOR_REGISTRATION: + loading = loadingOpenChallenges; + loadMore = loadMoreOpen; + break; default: break; } @@ -76,7 +97,7 @@ function Listing({ selectBucket(bucket)} @@ -96,13 +117,14 @@ function Listing({ setSort={sort => setSort(bucket, sort)} sort={sorts[bucket]} userHandle={_.get(auth, 'user.handle')} + loadMoreChallenges={loadMoreChallenges} /> ) ); }; if ((activeBucket !== BUCKETS.ALL) - && (activeBucket !== BUCKETS.SAVED_FILTER)) { + && (activeBucket !== BUCKETS.SAVED_FILTER)) { return (
{getBucket(activeBucket, true)} @@ -122,7 +144,7 @@ function Listing({ } Listing.defaultProps = { - challenges: [], + challenges: {}, communityName: null, // currentFilterName: '', // expanded: false, @@ -130,12 +152,16 @@ Listing.defaultProps = { expandTag: null, extraBucket: null, loadMorePast: null, + loadMoreMy: null, + loadMoreOpen: null, + loadMoreOnGoing: null, loadMoreReviewOpportunities: null, preListingMsg: null, reviewOpportunities: [], // onTechTagClicked: _.noop, // onExpandFilterResult: _.noop, openChallengesInNewTabs: false, + loadMoreChallenges: null, }; Listing.propTypes = { @@ -146,7 +172,7 @@ Listing.propTypes = { handle: PT.string, }), }).isRequired, - challenges: PT.arrayOf(PT.shape()), + challenges: PT.shape(), challengesUrl: PT.string.isRequired, communityName: PT.string, expandedTags: PT.arrayOf(PT.number), @@ -155,8 +181,14 @@ Listing.propTypes = { filterState: PT.shape().isRequired, keepPastPlaceholders: PT.bool.isRequired, loadingPastChallenges: PT.bool.isRequired, + loadingMyChallenges: PT.bool.isRequired, + loadingOnGoingChallenges: PT.bool.isRequired, + loadingOpenChallenges: PT.bool.isRequired, loadingReviewOpportunities: PT.bool.isRequired, loadMorePast: PT.func, + loadMoreMy: PT.func, + loadMoreOpen: PT.func, + loadMoreOnGoing: PT.func, loadMoreReviewOpportunities: PT.func, newChallengeDetails: PT.bool.isRequired, openChallengesInNewTabs: PT.bool, @@ -169,6 +201,7 @@ Listing.propTypes = { setFilterState: PT.func.isRequired, setSort: PT.func.isRequired, sorts: PT.shape().isRequired, + loadMoreChallenges: PT.func, }; const mapStateToProps = (state) => { diff --git a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx index f5066d9117..26ffbd8ee0 100644 --- a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx @@ -79,8 +79,8 @@ function ReviewOpportunityCard({ {start.format('MMM DD')} expandTag(challenge.id)} onTechTagClicked={onTechTagClicked} @@ -96,12 +96,12 @@ function ReviewOpportunityCard({
{payment.role} {' '} -- $ + - $ {payment.payment.toLocaleString()}
))}
-)} + )} >
@@ -109,7 +109,7 @@ $ {_.sumBy(opportunity.payments, 'payment').toLocaleString()}
-Payment + Payment
@@ -122,7 +122,7 @@ Payment
{quantityText(opportunity.openPositions, 'open position')}
-)} + )} > @@ -134,7 +134,7 @@ Payment
{quantityText(opportunity.submissions, 'submission')}
-)} + )} > diff --git a/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx b/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx index 1fd89a1850..28dffee403 100644 --- a/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx +++ b/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx @@ -6,7 +6,6 @@ import PT from 'prop-types'; import React from 'react'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; import { challenge as challengeUtils } from 'topcoder-react-lib'; import Bucket from './Bucket'; @@ -14,6 +13,7 @@ import Bucket from './Bucket'; import './style.scss'; const Filter = challengeUtils.filter; +const Buckets = challengeUtils.buckets; const RSS_LINK = 'http://feeds.topcoder.com/challenges/feed?list=active&contestType=all'; @@ -31,6 +31,7 @@ export default function BucketSelector({ selectBucket, selectSavedFilter, setEditSavedFiltersMode, + loadMoreChallenges, }) { let filteredChallenges = challenges.filter(Filter.getFilterFunction(filterState)); @@ -46,6 +47,7 @@ export default function BucketSelector({ disabled={disabled} onClick={() => { selectBucket(bucket); + loadMoreChallenges(bucket); /* eslint-env browser */ document.body.scrollTop = 0; document.documentElement.scrollTop = 0; @@ -56,9 +58,9 @@ export default function BucketSelector({ const savedFiltersRender = savedFilters.map((item, index) => ( - {getBucket(BUCKETS.ALL)} - {isAuth ? getBucket(BUCKETS.MY) : null} + {getBucket(Buckets.BUCKETS.ALL)} + {isAuth ? getBucket(Buckets.BUCKETS.MY) : null} {extraBucket ? getBucket(extraBucket) : null} - {getBucket(BUCKETS.OPEN_FOR_REGISTRATION)} - {getBucket(BUCKETS.ONGOING)} + {getBucket(Buckets.BUCKETS.OPEN_FOR_REGISTRATION)} + {getBucket(Buckets.BUCKETS.ONGOING)}
- {getBucket(BUCKETS.REVIEW_OPPORTUNITIES)} - {getBucket(BUCKETS.PAST)} + {getBucket(Buckets.BUCKETS.REVIEW_OPPORTUNITIES)} + {getBucket(Buckets.BUCKETS.PAST)} {/* NOTE: We do not show upcoming challenges for now, for various reasons, * more political than technical ;) getBucket(BUCKETS.UPCOMING) */ @@ -92,7 +94,7 @@ export default function BucketSelector({ @@ -123,6 +125,7 @@ BucketSelector.defaultProps = { disabled: false, extraBucket: null, isAuth: false, + loadMoreChallenges: null, }; BucketSelector.propTypes = { @@ -141,4 +144,5 @@ BucketSelector.propTypes = { selectBucket: PT.func.isRequired, selectSavedFilter: PT.func.isRequired, setEditSavedFiltersMode: PT.func.isRequired, + loadMoreChallenges: PT.func, }; diff --git a/src/shared/components/challenge-listing/Sidebar/index.jsx b/src/shared/components/challenge-listing/Sidebar/index.jsx index a21f0ac70d..895647b6f9 100644 --- a/src/shared/components/challenge-listing/Sidebar/index.jsx +++ b/src/shared/components/challenge-listing/Sidebar/index.jsx @@ -47,6 +47,7 @@ export default function SideBarFilters({ setEditSavedFiltersMode, updateAllSavedFilters, updateSavedFilter, + loadMoreChallenges, }) { return (
@@ -79,6 +80,7 @@ export default function SideBarFilters({ selectBucket={selectBucket} selectSavedFilter={selectSavedFilter} setEditSavedFiltersMode={setEditSavedFiltersMode} + loadMoreChallenges={loadMoreChallenges} /> )}
@@ -122,4 +124,5 @@ SideBarFilters.propTypes = { setEditSavedFiltersMode: PT.func.isRequired, updateAllSavedFilters: PT.func.isRequired, updateSavedFilter: PT.func.isRequired, + loadMoreChallenges: PT.func.isRequired, }; diff --git a/src/shared/components/challenge-listing/Tags.jsx b/src/shared/components/challenge-listing/Tags.jsx index 9446ec1a7a..d189ba5bee 100644 --- a/src/shared/components/challenge-listing/Tags.jsx +++ b/src/shared/components/challenge-listing/Tags.jsx @@ -25,7 +25,7 @@ export default function Tags({ }; const renderTechnologies = () => { - const combined = _.union(technologies, platforms); + const combined = _.without(_.union(technologies, platforms), ''); if (combined.length) { let display = combined; diff --git a/src/shared/components/challenge-listing/index.jsx b/src/shared/components/challenge-listing/index.jsx index ae9c8f386a..5326edf36f 100644 --- a/src/shared/components/challenge-listing/index.jsx +++ b/src/shared/components/challenge-listing/index.jsx @@ -8,9 +8,8 @@ import moment from 'moment'; import React from 'react'; import PT from 'prop-types'; import Sticky from 'react-stickynode'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import Sidebar from 'containers/challenge-listing/Sidebar'; -import { isReviewOpportunitiesBucket } from 'utils/challenge-listing/buckets'; import { config } from 'topcoder-react-utils'; import Listing from './Listing'; @@ -19,7 +18,7 @@ import SRMCard from './SRMCard'; import './style.scss'; -const Filter = challengeUtils.filter; +const { isReviewOpportunitiesBucket } = challengeUtil.buckets; // Number of challenge placeholder card to display const CHALLENGE_PLACEHOLDER_COUNT = 8; @@ -29,26 +28,18 @@ export default function ChallengeListing(props) { activeBucket, auth, challenges: propChallenges, - communityFilter, communityName, defaultCommunityId, extraBucket, - filterState, hideSrm, hideTcLinksInFooter, keepPastPlaceholders, loadingChallenges, preListingMsg, + loadMoreChallenges, } = props; - let { challenges } = props; - - if (communityFilter) { - challenges = challenges.filter(Filter.getFilterFunction(props.communityFilter)); - } - - challenges = challenges.filter(Filter.getFilterFunction(filterState)); - + const { challenges } = props; const expanded = false; /* When we automatically reload cached challenge objects, we do not want to @@ -71,7 +62,8 @@ export default function ChallengeListing(props) { let challengeCardContainer; if (!expanded && loadingChallenges && !suppressPlaceholders - && !isReviewOpportunitiesBucket(activeBucket)) { // Skip, Review Opps are not auto-refreshed + && !isReviewOpportunitiesBucket(activeBucket)) { + // Skip, Review Opps are not auto-refreshed const challengeCards = _.range(CHALLENGE_PLACEHOLDER_COUNT) .map(key => ); challengeCardContainer = ( @@ -95,8 +87,14 @@ export default function ChallengeListing(props) { filterState={props.filterState} keepPastPlaceholders={keepPastPlaceholders} loadingPastChallenges={props.loadingPastChallenges} + loadingMyChallenges={props.loadingMyChallenges} + loadingOpenChallenges={props.loadingOpenChallenges} + loadingOnGoingChallenges={props.loadingOnGoingChallenges} loadingReviewOpportunities={props.loadingReviewOpportunities} loadMorePast={props.loadMorePast} + loadMoreMy={props.loadMoreMy} + loadMoreOpen={props.loadMoreOpen} + loadMoreOnGoing={props.loadMoreOnGoing} loadMoreReviewOpportunities={props.loadMoreReviewOpportunities} newChallengeDetails={props.newChallengeDetails} openChallengesInNewTabs={props.openChallengesInNewTabs} @@ -111,6 +109,7 @@ export default function ChallengeListing(props) { sorts={props.sorts} loadMoreActive={props.loadMoreActive} loadingActiveChallenges={props.loadingChallenges} + loadMoreChallenges={props.loadMoreChallenges} /> ); } @@ -139,14 +138,14 @@ export default function ChallengeListing(props) { {/* upcoming SRMs */}
-Upcoming SRMs + Upcoming SRMs
{ /* UpcomingSrm */ }
{/* past SRMs */}
-Past SRMs + Past SRMs
@@ -161,7 +160,7 @@ Past SRMs
- +
{challengeCardContainer} @@ -171,6 +170,7 @@ Past SRMs
@@ -181,11 +181,13 @@ Past SRMs ChallengeListing.defaultProps = { auth: null, - communityFilter: null, communityName: null, extraBucket: null, hideTcLinksInFooter: false, loadMorePast: null, + loadMoreMy: null, + loadMoreOpen: null, + loadMoreOnGoing: null, loadMoreReviewOpportunities: null, newChallengeDetails: false, openChallengesInNewTabs: false, @@ -195,13 +197,13 @@ ChallengeListing.defaultProps = { expandedTags: [], expandTag: null, loadMoreActive: null, + loadMoreChallenges: null, }; ChallengeListing.propTypes = { activeBucket: PT.string.isRequired, - challenges: PT.arrayOf(PT.shape()).isRequired, + challenges: PT.shape().isRequired, challengesUrl: PT.string.isRequired, - communityFilter: PT.shape(), communityName: PT.string, defaultCommunityId: PT.string.isRequired, expandedTags: PT.arrayOf(PT.number), @@ -214,8 +216,14 @@ ChallengeListing.propTypes = { lastUpdateOfActiveChallenges: PT.number.isRequired, loadingChallenges: PT.bool.isRequired, loadingPastChallenges: PT.bool.isRequired, + loadingMyChallenges: PT.bool.isRequired, + loadingOpenChallenges: PT.bool.isRequired, + loadingOnGoingChallenges: PT.bool.isRequired, loadingReviewOpportunities: PT.bool.isRequired, loadMorePast: PT.func, + loadMoreMy: PT.func, + loadMoreOpen: PT.func, + loadMoreOnGoing: PT.func, loadMoreReviewOpportunities: PT.func, newChallengeDetails: PT.bool, openChallengesInNewTabs: PT.bool, @@ -230,4 +238,5 @@ ChallengeListing.propTypes = { sorts: PT.shape().isRequired, auth: PT.shape(), loadMoreActive: PT.func, + loadMoreChallenges: PT.func, }; diff --git a/src/shared/containers/ChallengesBlock.jsx b/src/shared/containers/ChallengesBlock.jsx index dc15b997ad..ec5146e212 100644 --- a/src/shared/containers/ChallengesBlock.jsx +++ b/src/shared/containers/ChallengesBlock.jsx @@ -5,7 +5,7 @@ * enhanced for usability in other places. */ -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenge as challengeUtil } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import ChallengesBlock from 'components/ChallengesBlock'; import LoadingIndicator from 'components/LoadingIndicator'; @@ -14,7 +14,8 @@ import PT from 'prop-types'; import React from 'react'; import shortId from 'shortid'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const { BUCKETS } = challengeUtil.buckets; /* Holds cache time [ms] for the data demanded by this container. */ const MAXAGE = 30 * 60 * 1000; @@ -28,7 +29,7 @@ class ChallengesBlockContiner extends React.Component { tokenV3, } = this.props; if (Date.now() - lastUpdateOfActiveChallenges > MAXAGE - && !loadingActiveChallenges) getAllActiveChallenges(tokenV3); + && !loadingActiveChallenges) getAllActiveChallenges(tokenV3); } render() { @@ -77,7 +78,7 @@ function mapStateToProps(state) { return { challenges: state.challengeListing.challenges, lastUpdateOfActiveChallenges: - state.challengeListing.lastUpdateOfActiveChallenges, + state.challengeListing.lastUpdateOfActiveChallenges, loadingActiveChallenges: Boolean(state.challengeListing.loadingActiveChallengesUUID), tokenV3: state.auth.tokenV3, @@ -85,7 +86,7 @@ function mapStateToProps(state) { } function mapDispatchToActions(dispatch) { - const cla = challengeListingActions.challengeListing; + const cla = actions.challengeListing; const clsa = challengeListingSidebarActions.challengeListing.sidebar; return { getAllActiveChallenges: (tokenV3) => { diff --git a/src/shared/containers/Dashboard/index.jsx b/src/shared/containers/Dashboard/index.jsx index 0a540225c3..29c7de2589 100644 --- a/src/shared/containers/Dashboard/index.jsx +++ b/src/shared/containers/Dashboard/index.jsx @@ -11,7 +11,6 @@ import Dashboard from 'components/Dashboard'; import dashActions from 'actions/page/dashboard'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import LoadingIndicator from 'components/LoadingIndicator'; -import { actions } from 'topcoder-react-lib'; import PT from 'prop-types'; import qs from 'qs'; import React from 'react'; @@ -19,9 +18,7 @@ import rssActions from 'actions/rss'; import shortId from 'shortid'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; - -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenge as challengeUtil } from 'topcoder-react-lib'; import communityActions from 'actions/tc-communities'; import { isTokenExpired } from 'tc-accounts'; @@ -29,6 +26,8 @@ import { config, isomorphy } from 'topcoder-react-utils'; import './styles.scss'; +const { BUCKETS } = challengeUtil.buckets; + /* When mounted, this container triggers (re-)loading of various data it needs. * It will not reload any data already present in the Redux store, if they were * fetched more recently than this max age [ms]. */ @@ -51,7 +50,7 @@ function updateCommunityStats(props) { communities.forEach((community) => { const stats = communityStats[community.communityId]; if (stats && (stats.loadingUuid - || now - (stats.timestamp || 0) < CACHE_MAX_AGE)) return; + || now - (stats.timestamp || 0) < CACHE_MAX_AGE)) return; getCommunityStats(community, activeChallenges, tokenV3); }); } @@ -124,28 +123,28 @@ export class DashboardPageContainer extends React.Component { const now = Date.now(); if (now - achievementsTimestamp > CACHE_MAX_AGE - && !achievementsLoading) getMemberAchievements(handle); + && !achievementsLoading) getMemberAchievements(handle); if (now - activeChallengesTimestamp > CACHE_MAX_AGE - && !activeChallengesLoading) getAllActiveChallenges(tokenV3); + && !activeChallengesLoading) getAllActiveChallenges(tokenV3); if (now - communitiesTimestamp > CACHE_MAX_AGE - && !communitiesLoading) getCommunityList({ profile, tokenV3 }); + && !communitiesLoading) getCommunityList({ profile, tokenV3 }); if (now - financesTimestamp > CACHE_MAX_AGE - && !financesLoading) getMemberFinances(handle, tokenV3); + && !financesLoading) getMemberFinances(handle, tokenV3); if (now - srmsTimestamp > CACHE_MAX_AGE - && !srmsLoading) getSrms(handle, tokenV3); + && !srmsLoading) getSrms(handle, tokenV3); if (now - statsTimestamp > CACHE_MAX_AGE - && !statsLoading) getMemberStats(handle, tokenV3); + && !statsLoading) getMemberStats(handle, tokenV3); if (now - tcBlogTimestamp > CACHE_MAX_AGE - && !tcBlogLoading) getTopcoderBlogFeed(); + && !tcBlogLoading) getTopcoderBlogFeed(); if (now - communitiesTimestamp < CACHE_MAX_AGE - && now - activeChallengesTimestamp < CACHE_MAX_AGE) { + && now - activeChallengesTimestamp < CACHE_MAX_AGE) { updateCommunityStats(this.props); } } @@ -326,7 +325,7 @@ function mapStateToProps(state, props) { activeChallengesLoading: Boolean(state.challengeListing.loadingActiveChallengesUUID), activeChallengesTimestamp: - state.challengeListing.lastUpdateOfActiveChallenges, + state.challengeListing.lastUpdateOfActiveChallenges, authenticating: state.auth.authenticating, challengeFilter: dash.challengeFilter, communities: communities.data, @@ -365,8 +364,8 @@ function mapDispatchToProps(dispatch) { return { getAllActiveChallenges: (tokenV3) => { const uuid = shortId(); - dispatch(challengeListingActions.challengeListing.getAllActiveChallengesInit(uuid)); - dispatch(challengeListingActions.challengeListing.getAllActiveChallengesDone(uuid, tokenV3)); + dispatch(actions.challengeListing.getAllActiveChallengesInit(uuid)); + dispatch(actions.challengeListing.getAllActiveChallengesDone(uuid, tokenV3)); }, getCommunityList: (auth) => { const uuid = shortId(); @@ -396,7 +395,7 @@ function mapDispatchToProps(dispatch) { }, getSrms: (handle, tokenV3) => { const uuid = shortId(); - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; dispatch(a.getSrmsInit(uuid)); dispatch(a.getSrmsDone(uuid, handle, { filter: 'status=future', @@ -413,7 +412,7 @@ function mapDispatchToProps(dispatch) { selectChallengeDetailsTab: tab => dispatch(challengeDetailsActions.page.challengeDetails.selectTab(tab)), setChallengeListingFilter: (filter) => { - const cl = challengeListingActions.challengeListing; + const cl = actions.challengeListing; const cls = challengeListingSidebarActions.challengeListing.sidebar; dispatch(cl.setFilter(filter)); dispatch(cls.selectBucket(BUCKETS.ALL)); diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index c01abcd4f6..ea6f0545b6 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -11,7 +11,6 @@ import communityActions from 'actions/tc-communities'; import LoadingPagePlaceholder from 'components/LoadingPagePlaceholder'; import pageActions from 'actions/page'; import ChallengeHeader from 'components/challenge-detail/Header'; -import challengeListingActions from 'actions/challenge-listing'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import Registrants from 'components/challenge-detail/Registrants'; import shortId from 'shortid'; @@ -27,10 +26,9 @@ import PT from 'prop-types'; import { connect } from 'react-redux'; import challengeDetailsActions, { TABS as DETAIL_TABS } from 'actions/page/challenge-details'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; import { CHALLENGE_PHASE_TYPES, COMPETITION_TRACKS_V3, SUBTRACKS } from 'utils/tc'; import { config, MetaTags } from 'topcoder-react-utils'; -import { actions } from 'topcoder-react-lib'; +import { actions, challenges } from 'topcoder-react-lib'; import ogWireframe from '../../../assets/images/open-graph/challenges/01-wireframe.jpg'; @@ -54,11 +52,12 @@ import og48hUiPrototype from '../../../assets/images/open-graph/challenges/13-48h-ui-prototype-challenge.jpg'; /* A fallback image, just in case we missed some corner case. */ -import ogImage from - '../../../assets/images/og_image.jpg'; +import ogImage from '../../../assets/images/og_image.jpg'; import './styles.scss'; +const Buckets = challenges.buckets; + /* Holds various time ranges in milliseconds. */ const MIN = 60 * 1000; const DAY = 24 * 60 * MIN; @@ -153,7 +152,7 @@ class ChallengeDetailPageContainer extends React.Component { * currently available challenge details have been fetched without * authentication. */ || (auth.tokenV2 && auth.tokenV3 - && !challenge.fetchedWithAuth) + && !challenge.fetchedWithAuth) ) { loadChallengeDetails(auth, challengeId); @@ -289,87 +288,87 @@ class ChallengeDetailPageContainer extends React.Component { Challenge # {challengeId} {' '} -does not exist! + does not exist! )} { !isEmpty && ( - + ) } { !isEmpty && ( - unregisterFromChallenge(auth, challengeId) - } - unregistering={unregistering} - checkpoints={checkpoints} - hasRegistered={hasRegistered} - hasFirstPlacement={hasFirstPlacement} - challengeSubtracksMap={challengeSubtracksMap} - /> + unregisterFromChallenge(auth, challengeId) + } + unregistering={unregistering} + checkpoints={checkpoints} + hasRegistered={hasRegistered} + hasFirstPlacement={hasFirstPlacement} + challengeSubtracksMap={challengeSubtracksMap} + /> ) } { !isEmpty && selectedTab === DETAIL_TABS.DETAILS && ( - updateChallenge(x, auth.tokenV3)} - /> + updateChallenge(x, auth.tokenV3)} + /> ) } { !isEmpty && selectedTab === DETAIL_TABS.REGISTRANTS && ( - + ) } { !isEmpty && selectedTab === DETAIL_TABS.CHECKPOINTS && ( - + ) } { @@ -379,14 +378,14 @@ does not exist! { !isEmpty && !isLegacyMM && selectedTab === DETAIL_TABS.WINNERS && ( - + ) } @@ -541,10 +540,10 @@ const mapDispatchToProps = (dispatch) => { }); }, setChallengeListingFilter: (filter) => { - const cl = challengeListingActions.challengeListing; const cls = challengeListingSidebarActions.challengeListing.sidebar; + const cl = actions.challenge; dispatch(cl.setFilter(filter)); - dispatch(cls.selectBucket(BUCKETS.ALL)); + dispatch(cls.selectBucket(Buckets.BUCKETS.ALL)); }, setSpecsTabState: state => dispatch(pageActions.page.challengeDetails.setSpecsTabState(state)), unregisterFromChallenge: (auth, challengeId) => { @@ -573,7 +572,7 @@ const mapDispatchToProps = (dispatch) => { dispatch(selectTab(tab)); }, getSubtracks: () => { - const cl = challengeListingActions.challengeListing; + const cl = actions.challengeListing; dispatch(cl.getChallengeSubtracksInit()); dispatch(cl.getChallengeSubtracksDone()); }, diff --git a/src/shared/containers/challenge-listing/FilterPanel.jsx b/src/shared/containers/challenge-listing/FilterPanel.jsx index ef21f12729..3f4813b537 100644 --- a/src/shared/containers/challenge-listing/FilterPanel.jsx +++ b/src/shared/containers/challenge-listing/FilterPanel.jsx @@ -2,18 +2,19 @@ * Container for the header filters panel. */ /* global window */ - -import actions from 'actions/challenge-listing/filter-panel'; -import challengeListingActions from 'actions/challenge-listing'; +import _ from 'lodash'; +import pactions from 'actions/challenge-listing/filter-panel'; +import { actions, challenge as challengeUtil } from 'topcoder-react-lib'; import FilterPanel from 'components/challenge-listing/Filters/ChallengeFilters'; import PT from 'prop-types'; import React from 'react'; import sidebarActions from 'actions/challenge-listing/sidebar'; -import { BUCKETS, isReviewOpportunitiesBucket } from 'utils/challenge-listing/buckets'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import qs from 'qs'; +const { BUCKETS, isReviewOpportunitiesBucket } = challengeUtil.buckets; + /* The default name for user-saved challenge filters. An integer * number will be appended to it, when necessary, to keep filter * names unique. */ @@ -67,6 +68,7 @@ export class Container extends React.Component { selectedCommunityId, setFilterState, tokenV2, + challenges, } = this.props; const communityFilters2 = [ { @@ -82,6 +84,7 @@ export class Container extends React.Component { return ( { const name = getAvailableFilterName(savedFilters); @@ -133,12 +136,13 @@ Container.propTypes = { setFilterState: PT.func.isRequired, auth: PT.shape().isRequired, tokenV2: PT.string, + setDatepickerStatus: PT.func.isRequired, }; function mapDispatchToProps(dispatch) { - const a = actions.challengeListing.filterPanel; - const cla = challengeListingActions.challengeListing; - const sa = sidebarActions.challengeListing.sidebar; + const a = pactions.challengeListingFrontend.filterPanel; + const cla = actions.challengeListing; + const sa = sidebarActions.challengeListingFrontend.sidebar; return { ...bindActionCreators(a, dispatch), getSubtracks: () => { @@ -156,16 +160,19 @@ function mapDispatchToProps(dispatch) { selectBucket: bucket => dispatch(sa.selectBucket(bucket)), selectCommunity: id => dispatch(cla.selectCommunity(id)), setFilterState: s => dispatch(cla.setFilter(s)), + setDatepickerStatus: status => dispatch(cla.setDatepickerStatus(status)), }; } function mapStateToProps(state, ownProps) { const cl = state.challengeListing; + const clFrontend = state.challengeListingFrontend; const tc = state.tcCommunities; return { ...ownProps, - ...state.challengeListing.filterPanel, - activeBucket: cl.sidebar.activeBucket, + ...state.challengeListingFrontend.filterPanel, + challenges: _.has(cl.challenges, BUCKETS.ALL) ? cl.challenges[BUCKETS.ALL] : [], + activeBucket: clFrontend.sidebar.activeBucket, communityFilters: tc.list.data, defaultCommunityId: ownProps.defaultCommunityId, filterState: cl.filter, @@ -176,8 +183,8 @@ function mapStateToProps(state, ownProps) { selectedCommunityId: cl.selectedCommunityId, auth: state.auth, tokenV2: state.auth.tokenV2, - isSavingFilter: cl.sidebar.isSavingFilter, - savedFilters: cl.sidebar.savedFilters, + isSavingFilter: clFrontend.sidebar.isSavingFilter, + savedFilters: clFrontend.sidebar.savedFilters, }; } diff --git a/src/shared/containers/challenge-listing/Listing/index.jsx b/src/shared/containers/challenge-listing/Listing/index.jsx index 61bc3939ed..d0697fd61a 100644 --- a/src/shared/containers/challenge-listing/Listing/index.jsx +++ b/src/shared/containers/challenge-listing/Listing/index.jsx @@ -10,11 +10,10 @@ */ import _ from 'lodash'; -import actions from 'actions/challenge-listing'; import challengeDetailsActions from 'actions/page/challenge-details'; import filterPanelActions from 'actions/challenge-listing/filter-panel'; import headerActions from 'actions/topcoder_header'; -import { logger, challenge as challengeUtils } from 'topcoder-react-lib'; +import { logger, actions, challenge as challengeUtil } from 'topcoder-react-lib'; import React from 'react'; import PT from 'prop-types'; import shortId from 'shortid'; @@ -23,14 +22,13 @@ import ChallengeListing from 'components/challenge-listing'; import Banner from 'components/tc-communities/Banner'; import sidebarActions from 'actions/challenge-listing/sidebar'; import communityActions from 'actions/tc-communities'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; import { MetaTags } from 'topcoder-react-utils'; import { USER_GROUP_MAXAGE } from 'config'; import ogImage from '../../../../assets/images/og_image.jpg'; import style from './styles.scss'; -const { combine, mapToBackend } = challengeUtils.filter; +const { BUCKETS, BUCKET_DATA } = challengeUtil.buckets; let mounted = false; @@ -57,7 +55,7 @@ export class ListingContainer extends React.Component { } if (!communitiesList.loadingUuid - && (Date.now() - communitiesList.timestamp > USER_GROUP_MAXAGE)) { + && (Date.now() - communitiesList.timestamp > USER_GROUP_MAXAGE)) { getCommunitiesList(auth); } @@ -68,20 +66,67 @@ export class ListingContainer extends React.Component { if (mounted) { logger.error('Attempt to mount multiple instances of ChallengeListingPageContainer at the same time!'); } else mounted = true; - this.loadChallenges(); } componentDidUpdate(prevProps) { const { + activeBucket, auth, dropChallenges, getCommunitiesList, - allActiveChallengesLoaded, - getRestActiveChallenges, - meta, - loadingActiveChallengesUUID, + datepickerOpen, } = this.props; + + const oldFilter = _.get(prevProps, 'filter'); + const filter = _.get(this.props, 'filter'); + + const oldSorts = _.get(prevProps, 'sorts'); + const sorts = _.get(this.props, 'sorts'); + + const oldCommunityId = _.get(prevProps, 'selectedCommunityId'); + const newCommunityId = _.get(this.props, 'selectedCommunityId'); + + const oldActiveBucket = _.get(prevProps, 'activeBucket'); + + const newStartDate = _.get(filter, 'startDate'); + const newEndDate = _.get(filter, 'endDate'); + + let filterChanged = !_.isEqual(oldFilter, filter) + || newCommunityId !== oldCommunityId + || activeBucket !== oldActiveBucket + || !_.isEqual(oldSorts, sorts); + + if ( + (newStartDate && newStartDate === newEndDate) + || (newStartDate && newEndDate === undefined) + || datepickerOpen + ) { + filterChanged = false; + } + + if (filterChanged) { + switch (activeBucket) { + case BUCKETS.PAST: + dropChallenges(activeBucket); + this.loadPastChallenges(); + break; + case BUCKETS.ALL: + case BUCKETS.MY: + case BUCKETS.OPEN_FOR_REGISTRATION: + case BUCKETS.ONGOING: + dropChallenges(activeBucket); + this.loadChallenges(); + break; + case BUCKETS.REVIEW_OPPORTUNITIES: + dropChallenges(activeBucket); + this.loadReviewOpportunities(); + break; + default: + break; + } + } + const oldUserId = _.get(prevProps, 'auth.user.userId'); const userId = _.get(this.props, 'auth.user.userId'); if (userId !== oldUserId) { @@ -93,14 +138,10 @@ export class ListingContainer extends React.Component { if (!prevProps.auth.profile) setImmediate(() => this.loadChallenges()); } else if (prevProps.auth.profile) { setImmediate(() => { - dropChallenges(); + dropChallenges(activeBucket); this.loadChallenges(); }); } - - if (!loadingActiveChallengesUUID && !_.isEmpty(meta) && !allActiveChallengesLoaded) { - getRestActiveChallenges(auth.tokenV3); - } } componentWillUnmount() { @@ -114,42 +155,226 @@ export class ListingContainer extends React.Component { /* Evaluates the backend challenge filter most suitable for the current state * of the active frontend filters. */ getBackendFilter() { - const { - communitiesList, - selectedCommunityId, - } = this.props; - let { filter } = this.props; - let communityFilter = communitiesList.data.find( - item => item.communityId === selectedCommunityId, - ); - if (communityFilter) communityFilter = communityFilter.challengeFilter; - if (communityFilter) filter = combine(filter, communityFilter); + const { filter } = this.props; + const b = this.mappedFilter(); return { - back: mapToBackend(filter), + back: b, front: filter, }; } + // transform frontend filter to backend filter + mappedFilter() { + const { + filter, + communityFilters, + selectedCommunityId, + } = this.props; + + const f = {}; + if (!filter) { + return f; + } + if (filter.tags) { + f.keywords = filter.tags.join(','); + } + if (filter.subtracks) { + f.subTrack = filter.subtracks.join(','); + } + if (filter.tracks) { + const tracks = Object.keys(filter.tracks).map(t => t.toUpperCase()); + f.track = tracks.join(','); + } + if (filter.text) { + f.name = filter.text; + } + + if (selectedCommunityId !== '') { + const c = communityFilters.find(item => item.communityId === selectedCommunityId); + if (c) { + const cf = c.challengeFilter; + if (cf.or) { + // use tags + let t = cf.or.join(','); + if (f.keywords) { + t = `${f.keywords},${t}`; + } + f.keywords = t; + } else if (cf.groupIds && cf.groupIds.length !== 0) { + // use group ids + f.groupIds = cf.groupIds.join(','); + } + } + } + + return f; + } + + loadMoreChallenges(bucket) { + const { + getMoreChallenges, + allMyChallengesLoaded, + allOpenChallengesLoaded, + allOnGoingChallengesLoaded, + } = this.props; + switch (bucket) { + case BUCKETS.MY: + if (!allMyChallengesLoaded) { + getMoreChallenges(bucket); + } + break; + case BUCKETS.ONGOING: + if (!allOnGoingChallengesLoaded) { + getMoreChallenges(bucket); + } + break; + case BUCKETS.OPEN_FOR_REGISTRATION: + if (!allOpenChallengesLoaded) { + getMoreChallenges(bucket); + } + break; + default: + break; + } + } + loadChallenges() { const { auth, getActiveChallenges, - lastRequestedPageOfActiveChallenges, + activeBucket, + sorts, } = this.props; const f = this.getBackendFilter(); + let sort = null; + if (activeBucket !== BUCKETS.ALL) { + sort = _.has(sorts, activeBucket) + ? sorts.activeBucket : null; + } + // only load the first page getActiveChallenges( - 1 + lastRequestedPageOfActiveChallenges, + 0, f.back, auth.tokenV3, f.front, + sort, + activeBucket, ); } + loadPastChallenges() { + const { + sorts, getPastChallenges, auth, + } = this.props; + const { tokenV3 } = auth; + const f = this.getBackendFilter(); + const sort = _.has(sorts, BUCKETS.PAST) + ? sorts[BUCKETS.PAST] : BUCKET_DATA[BUCKETS.PAST].sorts[0]; + getPastChallenges( + 0, + f.back, + tokenV3, + f.front, + sort, + ); + } + + loadReviewOpportunities() { + const { + sorts, getReviewOpportunities, auth, + } = this.props; + const { tokenV3 } = auth; + const f = this.getBackendFilter(); + const sort = _.has(sorts, BUCKETS.REVIEW_OPPORTUNITIES) + ? sorts[BUCKETS.REVIEW_OPPORTUNITIES] + : BUCKET_DATA[BUCKETS.REVIEW_OPPORTUNITIES].sorts[0]; + getReviewOpportunities( + 0, tokenV3, sort, f.front, + ); + } + + createLoadMoreFunction(bucket) { + const { + sorts, getReviewOpportunities, auth, lastRequestedPageOfReviewOpportunities, + getPastChallenges, lastRequestedPageOfPastChallenges, + getActiveChallenges, lastRequestedPageOfMyChallenges, + lastRequestedPageOfOnGoingChallenges, lastRequestedPageOfOpenChallenges, + } = this.props; + const { tokenV3 } = auth; + const f = this.getBackendFilter(); + const sort = _.has(sorts, bucket) + ? sorts[bucket] : BUCKET_DATA[bucket].sorts[0]; + let loadMore; + switch (bucket) { + case BUCKETS.PAST: + loadMore = () => { + getPastChallenges( + 1 + lastRequestedPageOfPastChallenges, + f.back, + tokenV3, + f.front, + sort, + ); + }; + break; + case BUCKETS.REVIEW_OPPORTUNITIES: + loadMore = () => { + getReviewOpportunities( + 1 + lastRequestedPageOfReviewOpportunities, tokenV3, sort, f.front, + ); + }; + break; + case BUCKETS.MY: + loadMore = () => { + getActiveChallenges( + 1 + lastRequestedPageOfMyChallenges, + f.back, + tokenV3, + f.front, + sort, + bucket, + ); + }; + break; + case BUCKETS.OPEN_FOR_REGISTRATION: + loadMore = () => { + getActiveChallenges( + 1 + lastRequestedPageOfOpenChallenges, + f.back, + tokenV3, + f.front, + sort, + bucket, + ); + }; + break; + case BUCKETS.ONGOING: + loadMore = () => { + getActiveChallenges( + 1 + lastRequestedPageOfOnGoingChallenges, + f.back, + tokenV3, + f.front, + sort, + bucket, + ); + }; + break; + default: + break; + } + + return loadMore; + } + render() { const { auth, allPastChallengesLoaded, allReviewOpportunitiesLoaded, + allOnGoingChallengesLoaded, + allMyChallengesLoaded, + allOpenChallengesLoaded, activeBucket, ChallengeListingBanner, challenges, @@ -165,14 +390,13 @@ export class ListingContainer extends React.Component { extraBucket, filter, groupIds, - getPastChallenges, - getReviewOpportunities, hideSrm, keepPastPlaceholders, - lastRequestedPageOfPastChallenges, - lastRequestedPageOfReviewOpportunities, lastUpdateOfActiveChallenges, loadingActiveChallengesUUID, + loadingMyChallengesUUID, + loadingOpenChallengesUUID, + loadingOnGoingChallengesUUID, loadingPastChallengesUUID, loadingReviewOpportunitiesUUID, listingOnly, @@ -189,28 +413,34 @@ export class ListingContainer extends React.Component { setSort, sorts, hideTcLinksInSidebarFooter, + gettingMoreMyChallenges, + gettingMoreOnGoingChallenges, + gettingMoreOpenChallenges, } = this.props; - const { tokenV3 } = auth; - let loadMorePast; if (!allPastChallengesLoaded) { - loadMorePast = () => { - const f = this.getBackendFilter(); - getPastChallenges( - 1 + lastRequestedPageOfPastChallenges, - f.back, - tokenV3, - f.front, - ); - }; + loadMorePast = this.createLoadMoreFunction(BUCKETS.PAST); } let loadMoreReviewOpportunities; if (!allReviewOpportunitiesLoaded) { - loadMoreReviewOpportunities = () => getReviewOpportunities( - 1 + lastRequestedPageOfReviewOpportunities, tokenV3, - ); + loadMoreReviewOpportunities = this.createLoadMoreFunction(BUCKETS.REVIEW_OPPORTUNITIES); + } + + let loadMoreMy; + if (!allMyChallengesLoaded && gettingMoreMyChallenges) { + loadMoreMy = this.createLoadMoreFunction(BUCKETS.MY); + } + + let loadMoreOpen; + if (!allOpenChallengesLoaded && gettingMoreOpenChallenges) { + loadMoreOpen = this.createLoadMoreFunction(BUCKETS.OPEN_FOR_REGISTRATION); + } + + let loadMoreOnGoing; + if (!allOnGoingChallengesLoaded && gettingMoreOnGoingChallenges) { + loadMoreOnGoing = this.createLoadMoreFunction(BUCKETS.ONGOING); } let communityFilter = communityFilters.find(item => item.communityId === selectedCommunityId); @@ -264,6 +494,9 @@ export class ListingContainer extends React.Component { lastUpdateOfActiveChallenges={lastUpdateOfActiveChallenges} loadingChallenges={Boolean(loadingActiveChallengesUUID)} loadingPastChallenges={Boolean(loadingPastChallengesUUID)} + loadingMyChallenges={Boolean(loadingMyChallengesUUID)} + loadingOnGoingChallenges={Boolean(loadingOnGoingChallengesUUID)} + loadingOpenChallenges={Boolean(loadingOpenChallengesUUID)} loadingReviewOpportunities={Boolean(loadingReviewOpportunitiesUUID)} newChallengeDetails={newChallengeDetails} openChallengesInNewTabs={openChallengesInNewTabs} @@ -274,6 +507,10 @@ export class ListingContainer extends React.Component { selectedCommunityId={selectedCommunityId} loadMorePast={loadMorePast} loadMoreReviewOpportunities={loadMoreReviewOpportunities} + loadMoreChallenges={bucket => this.loadMoreChallenges(bucket)} + loadMoreMy={loadMoreMy} + loadMoreOpen={loadMoreOpen} + loadMoreOnGoing={loadMoreOnGoing} reviewOpportunities={reviewOpportunities} setFilterState={(state) => { setFilter(state); @@ -311,7 +548,6 @@ ListingContainer.defaultProps = { preListingMsg: null, prizeMode: 'money-usd', queryBucket: BUCKETS.ALL, - meta: {}, }; ListingContainer.propTypes = { @@ -320,11 +556,13 @@ ListingContainer.propTypes = { tokenV3: PT.string, user: PT.shape(), }).isRequired, - allActiveChallengesLoaded: PT.bool.isRequired, + allMyChallengesLoaded: PT.bool.isRequired, + allOnGoingChallengesLoaded: PT.bool.isRequired, + allOpenChallengesLoaded: PT.bool.isRequired, allPastChallengesLoaded: PT.bool.isRequired, allReviewOpportunitiesLoaded: PT.bool.isRequired, ChallengeListingBanner: PT.node, - challenges: PT.arrayOf(PT.shape({})).isRequired, + challenges: PT.shape().isRequired, challengesUrl: PT.string, challengeSubtracks: PT.arrayOf(PT.shape()).isRequired, challengeTags: PT.arrayOf(PT.string).isRequired, @@ -346,16 +584,20 @@ ListingContainer.propTypes = { communityFilters: PT.arrayOf(PT.object).isRequired, extraBucket: PT.string, getActiveChallenges: PT.func.isRequired, - getRestActiveChallenges: PT.func.isRequired, getCommunitiesList: PT.func.isRequired, getPastChallenges: PT.func.isRequired, getReviewOpportunities: PT.func.isRequired, keepPastPlaceholders: PT.bool.isRequired, - lastRequestedPageOfActiveChallenges: PT.number.isRequired, lastRequestedPageOfPastChallenges: PT.number.isRequired, + lastRequestedPageOfMyChallenges: PT.number.isRequired, + lastRequestedPageOfOpenChallenges: PT.number.isRequired, + lastRequestedPageOfOnGoingChallenges: PT.number.isRequired, lastRequestedPageOfReviewOpportunities: PT.number.isRequired, lastUpdateOfActiveChallenges: PT.number.isRequired, loadingActiveChallengesUUID: PT.string.isRequired, + loadingMyChallengesUUID: PT.string.isRequired, + loadingOpenChallengesUUID: PT.string.isRequired, + loadingOnGoingChallengesUUID: PT.string.isRequired, loadingPastChallengesUUID: PT.string.isRequired, loadingReviewOpportunitiesUUID: PT.string.isRequired, markHeaderMenu: PT.func.isRequired, @@ -378,14 +620,22 @@ ListingContainer.propTypes = { expandedTags: PT.arrayOf(PT.number).isRequired, expandTag: PT.func.isRequired, queryBucket: PT.string, - meta: PT.shape(), + gettingMoreMyChallenges: PT.bool.isRequired, + gettingMoreOnGoingChallenges: PT.bool.isRequired, + gettingMoreOpenChallenges: PT.bool.isRequired, + getMoreChallenges: PT.func.isRequired, + datepickerOpen: PT.bool.isRequired, }; const mapStateToProps = (state, ownProps) => { const cl = state.challengeListing; + const clFrontend = state.challengeListingFrontend; const tc = state.tcCommunities; return { auth: state.auth, + allMyChallengesLoaded: cl.allMyChallengesLoaded, + allOnGoingChallengesLoaded: cl.allOnGoingChallengesLoaded, + allOpenChallengesLoaded: cl.allOpenChallengesLoaded, allActiveChallengesLoaded: cl.allActiveChallengesLoaded, allPastChallengesLoaded: cl.allPastChallengesLoaded, allReviewOpportunitiesLoaded: cl.allReviewOpportunitiesLoaded, @@ -400,10 +650,17 @@ const mapStateToProps = (state, ownProps) => { hideTcLinksInSidebarFooter: ownProps.hideTcLinksInSidebarFooter, keepPastPlaceholders: cl.keepPastPlaceholders, lastRequestedPageOfActiveChallenges: cl.lastRequestedPageOfActiveChallenges, + lastRequestedPageOfMyChallenges: cl.lastRequestedPageOfMyChallenges, + lastRequestedPageOfOnGoingChallenges: cl.lastRequestedPageOfOnGoingChallenges, + lastRequestedPageOfOpenChallenges: cl.lastRequestedPageOfOpenChallenges, lastRequestedPageOfPastChallenges: cl.lastRequestedPageOfPastChallenges, lastRequestedPageOfReviewOpportunities: cl.lastRequestedPageOfReviewOpportunities, lastUpdateOfActiveChallenges: cl.lastUpdateOfActiveChallenges, loadingActiveChallengesUUID: cl.loadingActiveChallengesUUID, + loadingMyChallengesUUID: cl.loadingMyChallengesUUID, + loadingOpenChallengesUUID: cl.loadingOpenChallengesUUID, + loadingOnGoingChallengesUUID: cl.loadingOnGoingChallengesUUID, + loadingRestActiveChallengesUUID: cl.loadingRestActiveChallengesUUID, loadingPastChallengesUUID: cl.loadingPastChallengesUUID, loadingReviewOpportunitiesUUID: cl.loadingReviewOpportunitiesUUID, loadingChallengeSubtracks: cl.loadingChallengeSubtracks, @@ -415,44 +672,58 @@ const mapStateToProps = (state, ownProps) => { reviewOpportunities: cl.reviewOpportunities, selectedCommunityId: cl.selectedCommunityId, sorts: cl.sorts, - activeBucket: cl.sidebar.activeBucket, + activeBucket: clFrontend.sidebar.activeBucket, expandedTags: cl.expandedTags, meta: cl.meta, + gettingMoreChallenges: cl.gettingMoreChallenges, + gettingMoreMyChallenges: cl.gettingMoreMyChallenges, + gettingMoreOpenChallenges: cl.gettingMoreOpenChallenges, + gettingMoreOnGoingChallenges: cl.gettingMoreOnGoingChallenges, + datepickerOpen: cl.datepickerOpen, }; }; function mapDispatchToProps(dispatch) { const a = actions.challengeListing; const ah = headerActions.topcoderHeader; - const fpa = filterPanelActions.challengeListing.filterPanel; - const sa = sidebarActions.challengeListing.sidebar; + const fpa = filterPanelActions.challengeListingFrontend.filterPanel; + const sa = sidebarActions.challengeListingFrontend.sidebar; const ca = communityActions.tcCommunity; return { - dropChallenges: () => dispatch(a.dropChallenges()), - getActiveChallenges: (page, filter, token, frontFilter) => { - const uuid = shortId(); - dispatch(a.getActiveChallengesInit(uuid, page, frontFilter)); - dispatch(a.getActiveChallengesDone(uuid, page, filter, token, frontFilter)); + dropChallenges: bucket => dispatch(a.dropChallenges(bucket)), + getMoreChallenges: bucket => dispatch(a.getMoreChallenges(bucket)), + getActiveChallenges: (page, filter, token, frontFilter, sort, bucket) => { + if (filter.track !== '') { + const uuid = shortId(); + dispatch(a.getActiveChallengesInit(uuid, page, frontFilter, sort, bucket)); + dispatch(a.getActiveChallengesDone( + uuid, page, filter, token, frontFilter, sort, bucket, + )); + } }, - getRestActiveChallenges: (token) => { - const uuid = shortId(); - dispatch(a.getRestActiveChallengesInit(uuid)); - dispatch(a.getRestActiveChallengesDone(uuid, token)); + getRestActiveChallenges: (token, backendFilter, frontFilter, sort, bucket) => { + if (backendFilter.track !== '') { + const uuid = shortId(); + dispatch(a.getRestActiveChallengesInit(uuid)); + dispatch(a.getRestActiveChallengesDone( + uuid, token, backendFilter, frontFilter, sort, bucket, + )); + } }, getCommunitiesList: (auth) => { const uuid = shortId(); dispatch(ca.getListInit(uuid)); dispatch(ca.getListDone(uuid, auth)); }, - getPastChallenges: (page, filter, token, frontFilter) => { + getPastChallenges: (page, filter, token, frontFilter, sort) => { const uuid = shortId(); - dispatch(a.getPastChallengesInit(uuid, page, frontFilter)); - dispatch(a.getPastChallengesDone(uuid, page, filter, token, frontFilter)); + dispatch(a.getPastChallengesInit(uuid, page, frontFilter, sort)); + dispatch(a.getPastChallengesDone(uuid, page, filter, token, frontFilter, sort)); }, - getReviewOpportunities: (page, token) => { + getReviewOpportunities: (page, token, sort, frontFilter) => { const uuid = shortId(); - dispatch(a.getReviewOpportunitiesInit(uuid, page)); - dispatch(a.getReviewOpportunitiesDone(uuid, page, token)); + dispatch(a.getReviewOpportunitiesInit(uuid, page, sort)); + dispatch(a.getReviewOpportunitiesDone(uuid, page, token, sort, frontFilter)); }, selectBucket: bucket => dispatch(sa.selectBucket(bucket)), selectChallengeDetailsTab: diff --git a/src/shared/containers/challenge-listing/Sidebar.jsx b/src/shared/containers/challenge-listing/Sidebar.jsx index 847a648b52..039b4ee37e 100644 --- a/src/shared/containers/challenge-listing/Sidebar.jsx +++ b/src/shared/containers/challenge-listing/Sidebar.jsx @@ -3,8 +3,8 @@ */ import _ from 'lodash'; -import actions from 'actions/challenge-listing/sidebar'; -import challengeListingActions from 'actions/challenge-listing'; +import sactions from 'actions/challenge-listing/sidebar'; +import { actions, challenge as challengeUtil } from 'topcoder-react-lib'; import { config } from 'topcoder-react-utils'; import filterPanelActions from 'actions/challenge-listing/filter-panel'; import PT from 'prop-types'; @@ -12,10 +12,11 @@ import React from 'react'; import Sidebar from 'components/challenge-listing/Sidebar'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { BUCKETS, getBuckets } from 'utils/challenge-listing/buckets'; export const SidebarPureComponent = Sidebar; +const { BUCKETS, getBuckets } = challengeUtil.buckets; + /** * Checks for errors in saved filters * @param {Array} savedFilters @@ -137,9 +138,9 @@ SidebarContainer.propTypes = { }; function mapDispatchToProps(dispatch) { - const a = actions.challengeListing.sidebar; - const cla = challengeListingActions.challengeListing; - const fpa = filterPanelActions.challengeListing.filterPanel; + const a = sactions.challengeListingFrontend.sidebar; + const cla = actions.challengeListing; + const fpa = filterPanelActions.challengeListingFrontend.filterPanel; return { ...bindActionCreators(a, dispatch), setFilter: filter => dispatch(cla.setFilter(filter)), @@ -149,11 +150,12 @@ function mapDispatchToProps(dispatch) { } function mapStateToProps(state, ownProps) { - const { activeBucket } = state.challengeListing.sidebar; + const { activeBucket } = state.challengeListingFrontend.sidebar; const pending = _.keys(state.challengeListing.pendingRequests); return { - ...state.challengeListing.sidebar, - challenges: state.challengeListing.challenges, + ...state.challengeListingFrontend.sidebar, + challenges: _.has(state.challengeListing.challenges, BUCKETS.ALL) + ? state.challengeListing.challenges[BUCKETS.ALL] : [], disabled: (activeBucket === BUCKETS.ALL) && Boolean(pending.length), extraBucket: ownProps.extraBucket, hideTcLinksInFooter: ownProps.hideTcLinksInFooter, diff --git a/src/shared/containers/tc-communities/CommunityStats.jsx b/src/shared/containers/tc-communities/CommunityStats.jsx index eed92b3546..a33b9684aa 100644 --- a/src/shared/containers/tc-communities/CommunityStats.jsx +++ b/src/shared/containers/tc-communities/CommunityStats.jsx @@ -12,10 +12,11 @@ import React from 'react'; import { connect } from 'react-redux'; import shortid from 'shortid'; -import { actions } from 'topcoder-react-lib'; -import cActions from 'actions/challenge-listing'; +import { actions, challenge as challengeUtil } from 'topcoder-react-lib'; import CommunityStats from 'components/tc-communities/CommunityStats'; +const { BUCKETS } = challengeUtil.buckets; + /* Various time ranges in ms. */ const MIN = 60 * 1000; @@ -122,8 +123,8 @@ function mapDispatchToProps(dispatch) { }, getAllActiveChallenges: (token) => { const uuid = shortid(); - dispatch(cActions.challengeListing.getAllActiveChallengesInit(uuid)); - dispatch(cActions.challengeListing.getAllActiveChallengesDone(uuid, token)); + dispatch(actions.challengeListing.getAllActiveChallengesInit(uuid)); + dispatch(actions.challengeListing.getAllActiveChallengesDone(uuid, token)); }, }; } @@ -133,7 +134,7 @@ function mapStateToProps(state, ownProps) { const { challenges, lastUpdateOfActiveChallenges } = state.challengeListing; return { community, - challenges, + challenges: _.has(challenges, BUCKETS.ALL) ? challenges[BUCKETS.ALL] : [], lastUpdateOfActiveChallenges, loadingChallenges: Boolean(state.challengeListing.loadingActiveChallengesUUID), stats: ownProps.stats || _.get(state.stats.communities[community.communityId], 'data'), diff --git a/src/shared/containers/tc-communities/cognitive/home.jsx b/src/shared/containers/tc-communities/cognitive/home.jsx index 7d24ed331c..9fef2f961f 100644 --- a/src/shared/containers/tc-communities/cognitive/home.jsx +++ b/src/shared/containers/tc-communities/cognitive/home.jsx @@ -1,5 +1,4 @@ import _ from 'lodash'; -import challengeListingActions from 'actions/challenge-listing'; import communityActions from 'actions/tc-communities'; import Home from 'components/tc-communities/communities/cognitive/Home'; import moment from 'moment'; @@ -10,7 +9,7 @@ import shortId from 'shortid'; import { USER_GROUP_MAXAGE } from 'config'; import { connect } from 'react-redux'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtils, actions } from 'topcoder-react-lib'; /* Holds cache time [ms] for the data demanded by this container. */ const MAXAGE = 30 * 60 * 1000; @@ -28,11 +27,11 @@ class HomeContainer extends React.Component { loadingActiveChallenges, } = this.props; if (Date.now() - activeChallengesTimestamp > MAXAGE - && !loadingActiveChallenges) { + && !loadingActiveChallenges) { getAllActiveChallenges(auth.tokenV3); } if (Date.now() - communitiesList.timestamp > USER_GROUP_MAXAGE - && !communitiesList.loadingUuid) { + && !communitiesList.loadingUuid) { getCommunitiesList(auth); } } @@ -99,7 +98,7 @@ function mapStateToProps(state) { auth: state.auth, activeChallenges: state.challengeListing.challenges, activeChallengesTimestamp: - state.challengeListing.lastUpdateOfActiveChallenges, + state.challengeListing.lastUpdateOfActiveChallenges, allFaqItemsClosedInResourcesPage: _.isEmpty(state.page.communities.cognitive.resources.shownFaqItems), communitiesList: state.tcCommunities.list, @@ -110,7 +109,7 @@ function mapStateToProps(state) { function mapDispatchToActions(dispatch) { const ca = communityActions.tcCommunity; - const cla = challengeListingActions.challengeListing; + const cla = actions.challengeListing; const ra = resourcesActions.page.communities.cognitive.resources; return { closeAllFaqItemsInResourcesPage: () => dispatch(ra.closeAllFaqItems()), diff --git a/src/shared/containers/tc-communities/cs/Home.js b/src/shared/containers/tc-communities/cs/Home.js index 4eab16db01..d080b52791 100644 --- a/src/shared/containers/tc-communities/cs/Home.js +++ b/src/shared/containers/tc-communities/cs/Home.js @@ -1,17 +1,18 @@ -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenges } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import Home from 'components/tc-communities/communities/cs/Home'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const Buckets = challenges.buckets; function mapDispatchToProps(dispatch) { return { resetChallengeListing: () => { - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(Buckets.BUCKETS.ALL)); }, }; } diff --git a/src/shared/containers/tc-communities/iot/About.js b/src/shared/containers/tc-communities/iot/About.js index 2eeb7d4034..011ef3bb9a 100644 --- a/src/shared/containers/tc-communities/iot/About.js +++ b/src/shared/containers/tc-communities/iot/About.js @@ -1,9 +1,10 @@ import _ from 'lodash'; -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenges } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import About from 'components/tc-communities/communities/iot/About'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const Buckets = challenges.buckets; function mapStateToProps(state) { return { @@ -14,11 +15,11 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { resetChallengeListing: () => { - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(Buckets.BUCKETS.ALL)); }, }; } diff --git a/src/shared/containers/tc-communities/iot/AssetDetail.js b/src/shared/containers/tc-communities/iot/AssetDetail.js index 018792df46..860f5f8e88 100644 --- a/src/shared/containers/tc-communities/iot/AssetDetail.js +++ b/src/shared/containers/tc-communities/iot/AssetDetail.js @@ -1,9 +1,10 @@ import _ from 'lodash'; -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenges } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import AssetDetail from 'components/tc-communities/communities/iot/AssetDetail'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const Buckets = challenges.buckets; function mapStateToProps(state, ownProps) { @@ -16,11 +17,11 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { resetChallengeListing: () => { - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(Buckets.BUCKETS.ALL)); }, }; } diff --git a/src/shared/containers/tc-communities/iot/Assets.js b/src/shared/containers/tc-communities/iot/Assets.js index f4141e9c43..6a9f9b3756 100644 --- a/src/shared/containers/tc-communities/iot/Assets.js +++ b/src/shared/containers/tc-communities/iot/Assets.js @@ -1,10 +1,11 @@ -import actions from 'actions/page/communities/iot/assets'; +import pageAactions from 'actions/page/communities/iot/assets'; import _ from 'lodash'; -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenges } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import Assets from 'components/tc-communities/communities/iot/Assets'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const Buckets = challenges.buckets; function mapStateToProps(state) { return { @@ -16,18 +17,18 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { resetChallengeListing: () => { - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(Buckets.BUCKETS.ALL)); }, toggleGrid: () => { - const a = actions.page.communities.iot.assets; + const a = pageAactions.page.communities.iot.assets; dispatch(a.toggleGrid()); }, toggleList: () => { - const a = actions.page.communities.iot.assets; + const a = pageAactions.page.communities.iot.assets; dispatch(a.toggleList()); }, }; diff --git a/src/shared/containers/tc-communities/iot/GetStarted.js b/src/shared/containers/tc-communities/iot/GetStarted.js index 3efc23235f..0a5efc95e8 100644 --- a/src/shared/containers/tc-communities/iot/GetStarted.js +++ b/src/shared/containers/tc-communities/iot/GetStarted.js @@ -1,9 +1,10 @@ import _ from 'lodash'; -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenges } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import GetStarted from 'components/tc-communities/communities/iot/GetStarted'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const Buckets = challenges.buckets; function mapStateToProps(state) { return { @@ -14,11 +15,11 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { resetChallengeListing: () => { - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(Buckets.BUCKETS.ALL)); }, }; } diff --git a/src/shared/containers/tc-communities/iot/Home.js b/src/shared/containers/tc-communities/iot/Home.js index a9901d8b0f..7371b66ae1 100644 --- a/src/shared/containers/tc-communities/iot/Home.js +++ b/src/shared/containers/tc-communities/iot/Home.js @@ -1,9 +1,10 @@ import _ from 'lodash'; -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenges } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import Home from 'components/tc-communities/communities/iot/Home'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const Buckets = challenges.buckets; function mapStateToProps(state) { return { @@ -14,11 +15,11 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { resetChallengeListing: () => { - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(Buckets.BUCKETS.ALL)); }, }; } diff --git a/src/shared/containers/tc-communities/wipro/Home.js b/src/shared/containers/tc-communities/wipro/Home.js index 24f4a01ad3..4e5b54fb6f 100644 --- a/src/shared/containers/tc-communities/wipro/Home.js +++ b/src/shared/containers/tc-communities/wipro/Home.js @@ -1,9 +1,10 @@ import _ from 'lodash'; -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenges } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import Home from 'components/tc-communities/communities/wipro/Home'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const Buckets = challenges.buckets; function mapStateToProps(state) { return { @@ -14,11 +15,11 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { resetChallengeListing: () => { - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(Buckets.BUCKETS.ALL)); }, }; } diff --git a/src/shared/containers/tc-communities/zurich/Home.js b/src/shared/containers/tc-communities/zurich/Home.js index 61515fe8b2..5b82a369a6 100644 --- a/src/shared/containers/tc-communities/zurich/Home.js +++ b/src/shared/containers/tc-communities/zurich/Home.js @@ -1,17 +1,18 @@ -import challengeListingActions from 'actions/challenge-listing'; +import { actions, challenges } from 'topcoder-react-lib'; import challengeListingSidebarActions from 'actions/challenge-listing/sidebar'; import Home from 'components/tc-communities/communities/zurich/Home'; import { connect } from 'react-redux'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; + +const Buckets = challenges.buckets; function mapDispatchToProps(dispatch) { return { resetChallengeListing: () => { - const a = challengeListingActions.challengeListing; + const a = actions.challengeListing; const sa = challengeListingSidebarActions.challengeListing.sidebar; dispatch(a.selectCommunity('')); dispatch(a.setFilter({})); - dispatch(sa.selectBucket(BUCKETS.ALL)); + dispatch(sa.selectBucket(Buckets.BUCKETS.ALL)); }, }; } diff --git a/src/shared/reducers/challenge-listing/filter-panel.js b/src/shared/reducers/challenge-listing/filter-panel.js index 991ff3c8bf..265c090187 100644 --- a/src/shared/reducers/challenge-listing/filter-panel.js +++ b/src/shared/reducers/challenge-listing/filter-panel.js @@ -12,7 +12,7 @@ import { handleActions } from 'redux-actions'; * @return {Function} Reducer. */ function create(initialState = {}) { - const a = actions.challengeListing.filterPanel; + const a = actions.challengeListingFrontend.filterPanel; return handleActions({ [a.setExpanded]: (state, { payload }) => ({ ...state, expanded: payload }), [a.setSearchText]: (state, { payload }) => ({ ...state, searchText: payload }), diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js index 524e62ce54..e56f2f53ab 100644 --- a/src/shared/reducers/challenge-listing/index.js +++ b/src/shared/reducers/challenge-listing/index.js @@ -1,521 +1,34 @@ /** - * Reducer for state.challengeListing. + * Redux Reducer for state.challenge-list frontend + * + * Description: + * Implements reducer factory for the state.page segment of Redux state; and + * combines it with the child state.challengeListFrontend.x reducer factories. */ -import _ from 'lodash'; -import actions from 'actions/challenge-listing'; -import { handleActions } from 'redux-actions'; +import { combineReducers } from 'redux'; import { redux } from 'topcoder-react-utils'; -import { updateQuery } from 'utils/url'; -import moment from 'moment'; -import { logger, errors, challenge as challengeUtils } - from 'topcoder-react-lib'; -import filterPanel from './filter-panel'; +import filterPanel, { factory as filterPanelFactory } from './filter-panel'; import sidebar, { factory as sidebarFactory } from './sidebar'; -const { fireErrorMessage } = errors; -const { filter: Filter } = challengeUtils; - -/** TODO: Inspect if the 2 actions bellow can be removed? - * They do duplicate what is done in `getActiveChallengesDone` but fetch all challenges - * which was refactored in listing-improve - */ -function onGetAllActiveChallengesInit(state, { payload }) { - return { ...state, loadingActiveChallengesUUID: payload }; -} -function onGetAllActiveChallengesDone(state, { error, payload }) { - if (error) { - logger.error(payload); - return state; - } - const { uuid, challenges: loaded } = payload; - if (uuid !== state.loadingActiveChallengesUUID) return state; - /* Once all active challenges are fetched from the API, we remove from the - * store any active challenges stored there previously, and also any - * challenges with IDs matching any challenges loaded now as active. */ - const ids = new Set(); - loaded.forEach(item => ids.add(item.id)); - const challenges = state.challenges - .filter(item => item.status !== 'ACTIVE' && !ids.has(item.id)) - .concat(loaded); - - return { - ...state, - challenges, - lastUpdateOfActiveChallenges: Date.now(), - loadingActiveChallengesUUID: '', - }; -} - -/** - * Called when 1st page of ative challenges is loaded from `/challenges` api - * @param {*} state - * @param {*} param1 - */ -function onGetActiveChallengesDone(state, { error, payload }) { - if (error) { - logger.error(payload); - return state; - } - const { uuid, challenges: loaded } = payload; - if (uuid !== state.loadingActiveChallengesUUID) return state; - - /* Once all active challenges are fetched from the API, we remove from the - * store any active challenges stored there previously, and also any - * challenges with IDs matching any challenges loaded now as active. */ - const ids = new Set(); - loaded.forEach(item => ids.add(item.id)); - - /* Fetching 0 page of active challenges also drops any active challenges - * loaded to the state before. */ - const filter = state.lastRequestedPageOfActiveChallenges - ? item => !ids.has(item.id) - : item => !ids.has(item.id) && item.status !== 'ACTIVE'; - - const challenges = state.challenges - .filter(filter) - .concat(loaded); - - return { - ...state, - challenges, - lastUpdateOfActiveChallenges: Date.now(), - loadingActiveChallengesUUID: '', - meta: payload.meta, - }; -} - -/** - * Called when loading of 1st page of active challenges is started - * @param {*} state - * @param {*} param1 - */ -function onGetActiveChallengesInit(state, { payload }) { - return { - ...state, - loadingActiveChallengesUUID: payload.uuid, - lastRequestedPageOfActiveChallenges: payload.page, - }; -} -function onGetRestActiveChallengesInit(state, { payload }) { - return { - ...state, - loadingRestActiveChallengesUUID: payload.uuid, - }; -} - -/** - * Called when all challenges are loaded - * @param {*} state - * @param {*} param1 - */ -function onGetRestActiveChallengesDone(state, { error, payload }) { - if (error) { - logger.error(payload); - return state; - } - const { uuid, challenges: loaded } = payload; - if (uuid !== state.loadingRestActiveChallengesUUID) return state; - - /* Once all active challenges are fetched from the API, we remove from the - * store any active challenges stored there previously, and also any - * challenges with IDs matching any challenges loaded now as active. */ - const ids = new Set(); - loaded.forEach(item => ids.add(item.id)); - - /* Fetching 0 page of active challenges also drops any active challenges - * loaded to the state before. */ - const filter = item => !ids.has(item.id); - - const challenges = state.challenges - .filter(filter) - .concat(loaded); - - return { - ...state, - challenges, - allActiveChallengesLoaded: true, - lastUpdateOfActiveChallenges: Date.now(), - lastRequestedPageOfActiveChallenges: -1, - loadingRestActiveChallengesUUID: '', - }; -} - -/** - * Handles CHALLENGE_LISTING/GET_CHALLENGE_SUBTRACKS_DONE action. - * @param {Object} state - * @param {Object} action - * @return {Object} - */ -function onGetChallengeSubtracksDone(state, action) { - if (action.error) logger.error(action.payload); - return { - ...state, - challengeSubtracks: action.error ? [] : action.payload, - challengeSubtracksMap: action.error ? {} : _.keyBy(action.payload, 'subTrack'), - loadingChallengeSubtracks: false, - }; -} - -/** - * Handles CHALLENGE_LISTING/GET_CHALLENGE_TAGS_DONE action. - * @param {Object} state - * @param {Object} action - * @return {Object} - */ -function onGetChallengeTagsDone(state, action) { - if (action.error) logger.error(action.payload); - return { - ...state, - challengeTags: action.error ? [] : action.payload, - loadingChallengeTags: false, - }; -} - -function onGetPastChallengesInit(state, action) { - const { frontFilter, page, uuid } = action.payload; - const tracks = frontFilter && frontFilter.tracks; - if (tracks && _.isEmpty(tracks)) { - return { - ...state, - allPastChallengesLoaded: true, - loadingPastChallengesUUID: '', - }; - } - - return { - ...state, - lastRequestedPageOfPastChallenges: page, - loadingPastChallengesUUID: uuid, - }; -} - -function onGetPastChallengesDone(state, { error, payload }) { - if (error) { - logger.error(payload); - return state; - } - const { uuid, challenges: loaded, frontFilter } = payload; - if (uuid !== state.loadingPastChallengesUUID) return state; - - const ids = new Set(); - loaded.forEach(item => ids.add(item.id)); - - /* Fetching 0 page of past challenges also drops any past challenges - * loaded to the state before. */ - const filter = state.lastRequestedPageOfPastChallenges - ? item => !ids.has(item.id) - : item => !ids.has(item.id) && item.status !== 'COMPLETED' && item.status !== 'PAST'; - - const challenges = state.challenges.filter(filter).concat(loaded); - - let keepPastPlaceholders = false; - if (loaded.length) { - const ff = Filter.getFilterFunction(frontFilter); - keepPastPlaceholders = challenges.filter(ff).length - state.challenges.filter(ff).length < 10; - } - - return { - ...state, - allPastChallengesLoaded: loaded.length === 0, - challenges, - keepPastPlaceholders, - loadingPastChallengesUUID: '', - }; -} - -function onSelectCommunity(state, { payload }) { - updateQuery({ communityId: payload || undefined }); - return { - ...state, - selectedCommunityId: payload, - - /* Page numbers of past/upcoming challenges depend on the filters. To keep - * the code simple we just reset them each time a filter is modified. - * (This community selection defines community-specific filter for - * challenges). */ - allPastChallengesLoaded: false, - lastRequestedPageOfPastChallenges: -1, - }; -} - -/** - * @param {Object} state - * @param {Object} action - * @return {Object} - */ -function onSetFilter(state, { payload }) { - /* Validation of filter parameters: they may come from URL query, thus - * validation is not a bad idea. As you may note, at the moment we do not - * do it very carefuly (many params are not validated). */ - const filter = _.clone(payload); - if (_.isPlainObject(filter.tags)) { - filter.tags = _.values(filter.tags); - } - if (_.isPlainObject(filter.subtracks)) { - filter.subtracks = _.values(filter.subtracks); - } - if (filter.startDate && !moment(filter.startDate).isValid()) { - delete filter.startDate; - } - if (filter.endDate && !moment(filter.endDate).isValid()) { - delete filter.endDate; - } - - /* Update of URL and generation of the state. */ - updateQuery({ filter }); - return { - ...state, - filter, - - /* Page numbers of past/upcoming challenges depend on the filters. To keep - * the code simple we just reset them each time a filter is modified. */ - allPastChallengesLoaded: false, - lastRequestedPageOfPastChallenges: -1, - }; -} - /** - * Handles CHALLENGE_LISTING/GET_REVIEW_OPPORTUNITIES_INIT action. - * @param {Object} state - * @param {Object} action Payload will be page, uuid - * @return {Object} New state - */ -function onGetReviewOpportunitiesInit(state, { payload }) { - return { - ...state, - lastRequestedPageOfReviewOpportunities: payload.page, - loadingReviewOpportunitiesUUID: payload.uuid, - }; -} - -/** - * Handles CHALLENGE_LISTING/GET_REVIEW_OPPORTUNITIES_DONE action. - * @param {Object} state - * @param {Object} action Payload will be JSON from api call and UUID - * @return {Object} New state - */ -function onGetReviewOpportunitiesDone(state, { payload, error }) { - if (error) { - return state; - } - - const { - uuid, - loaded, - } = payload; - - if (uuid !== state.loadingReviewOpportunitiesUUID) return state; - - const ids = new Set(); - loaded.forEach(item => ids.add(item.id)); - const reviewOpportunities = state.reviewOpportunities - .filter(item => !ids.has(item.id)) - .concat(loaded); - - return { - ...state, - reviewOpportunities, - loadingReviewOpportunitiesUUID: '', - allReviewOpportunitiesLoaded: loaded.length === 0, - }; -} - -/** - * Inits the loading of SRMs. - * @param {Object} state - * @param {String} payload Operation UUID. - * @return {Object} New state. - */ -function onGetSrmsInit(state, { payload }) { - return { - ...state, - srms: { - ...state.srms, - loadingUuid: payload, - }, - }; -} - -/** - * Handles loaded SRMs. - * @param {Object} state - * @param {Object} action - * @return {Object} New state. - */ -function onGetSrmsDone(state, { error, payload }) { - if (error) { - logger.error('Failed to load SRMs', payload); - fireErrorMessage('Failed to load SRMs', ''); - return state; - } - - const { uuid, data } = payload; - if (state.srms.loadingUuid !== uuid) return state; - return { - ...state, - srms: { - data, - loadingUuid: '', - timestamp: Date.now(), - }, - }; -} - -/** - * Creates a new Challenge Listing reducer with the specified initial state. - * @param {Object} initialState Optional. Initial state. - * @return Challenge Listing reducer. - */ -function create(initialState) { - const a = actions.challengeListing; - return handleActions({ - [a.dropChallenges]: state => ({ - ...state, - allActiveChallengesLoaded: false, - allPastChallengesLoaded: false, - allReviewOpportunitiesLoaded: false, - challenges: [], - lastRequestedPageOfActiveChallenges: -1, - lastRequestedPageOfPastChallenges: -1, - lastRequestedPageOfReviewOpportunities: -1, - lastUpdateOfActiveChallenges: 0, - loadingActiveChallengesUUID: '', - loadingRestActiveChallengesUUID: '', - loadingPastChallengesUUID: '', - loadingReviewOpportunitiesUUID: '', - reviewOpportunities: [], - meta: { - allChallengesCount: 0, - myChallengesCount: 0, - ongoingChallengesCount: 0, - openChallengesCount: 0, - totalCount: 0, - }, - }), - - [a.expandTag]: (state, { payload }) => ({ - ...state, - expandedTags: [...state.expandedTags, payload], - }), - - [a.getAllActiveChallengesInit]: onGetAllActiveChallengesInit, - [a.getAllActiveChallengesDone]: onGetAllActiveChallengesDone, - - [a.getActiveChallengesInit]: onGetActiveChallengesInit, - [a.getActiveChallengesDone]: onGetActiveChallengesDone, - - [a.getRestActiveChallengesInit]: onGetRestActiveChallengesInit, - [a.getRestActiveChallengesDone]: onGetRestActiveChallengesDone, - - [a.getChallengeSubtracksInit]: state => ({ - ...state, - loadingChallengeSubtracks: true, - }), - [a.getChallengeSubtracksDone]: onGetChallengeSubtracksDone, - - [a.getChallengeTagsInit]: state => ({ - ...state, - loadingChallengeTags: true, - }), - [a.getChallengeTagsDone]: onGetChallengeTagsDone, - - [a.getPastChallengesInit]: onGetPastChallengesInit, - [a.getPastChallengesDone]: onGetPastChallengesDone, - - [a.getReviewOpportunitiesInit]: onGetReviewOpportunitiesInit, - [a.getReviewOpportunitiesDone]: onGetReviewOpportunitiesDone, - - [a.getSrmsInit]: onGetSrmsInit, - [a.getSrmsDone]: onGetSrmsDone, - - [a.selectCommunity]: onSelectCommunity, - - [a.setFilter]: onSetFilter, - [a.setSort]: (state, { payload }) => ({ - ...state, - sorts: { - ...state.sorts, - [payload.bucket]: payload.sort, - }, - }), - }, _.defaults(_.clone(initialState) || {}, { - allActiveChallengesLoaded: false, - allPastChallengesLoaded: false, - allReviewOpportunitiesLoaded: false, - - challenges: [], - challengeSubtracks: [], - challengeSubtracksMap: {}, - challengeTags: [], - - expandedTags: [], - - filter: {}, - - keepPastPlaceholders: false, - - lastRequestedPageOfActiveChallenges: -1, - lastRequestedPageOfPastChallenges: -1, - lastRequestedPageOfReviewOpportunities: -1, - lastUpdateOfActiveChallenges: 0, - - loadingActiveChallengesUUID: '', - loadingRestActiveChallengesUUID: '', - loadingPastChallengesUUID: '', - loadingReviewOpportunitiesUUID: '', - - loadingChallengeSubtracks: false, - loadingChallengeTags: false, - - reviewOpportunities: [], - - selectedCommunityId: '', - - sorts: {}, - - srms: { - data: [], - loadingUuid: '', - timestamp: 0, - }, - - meta: { - allChallengesCount: 0, - myChallengesCount: 0, - ongoingChallengesCount: 0, - openChallengesCount: 0, - totalCount: 0, - }, - })); -} - -/** - * The factory creates the new reducer with initial state tailored to the given - * ExpressJS HTTP request, if specified (for server-side rendering). If no HTTP - * request is specified, it creates the default reducer. - * @param {Object} req Optional. ExpressJS HTTP request. - * @return {Promise} Resolves to the new reducer. + * Reducer factory. + * @param {Object} req ExpressJS HTTP Request. + * @return {Function} Reducer. */ export function factory(req) { - if (req && req.url.match(/challenges(\/?$|\/?\?)/)) { - let state = {}; - - if (req.query.filter) { - state = onSetFilter(state, { payload: req.query.filter }); - } - state.selectedCommunityId = req.query.communityId; - - return redux.resolveReducers({ - sidebar: sidebarFactory(req), - }).then(reducers => redux.combineReducers(create(state), { ...reducers, filterPanel })); - } - return redux.resolveReducers({ + filterPanel: filterPanelFactory(req), sidebar: sidebarFactory(req), - }).then(reducers => redux.combineReducers(create(), { ...reducers, filterPanel })); + }).then(reducers => combineReducers({ + ...reducers, + filterPanel, + sidebar, + })); } -/* Default reducer with empty initial state. */ -export default redux.combineReducers(create(), { filterPanel, sidebar }); +export default combineReducers({ + filterPanel, + sidebar, +}); diff --git a/src/shared/reducers/challenge-listing/sidebar.js b/src/shared/reducers/challenge-listing/sidebar.js index 9114eeab5f..8ebe51030b 100644 --- a/src/shared/reducers/challenge-listing/sidebar.js +++ b/src/shared/reducers/challenge-listing/sidebar.js @@ -7,12 +7,12 @@ import _ from 'lodash'; import actions from 'actions/challenge-listing/sidebar'; -import { logger } from 'topcoder-react-lib'; -import { BUCKETS } from 'utils/challenge-listing/buckets'; +import { logger, challenge as challengeUtil, url } from 'topcoder-react-lib'; import { handleActions } from 'redux-actions'; -import { updateQuery } from 'utils/url'; const MAX_FILTER_NAME_LENGTH = 35; +const { BUCKETS } = challengeUtil.buckets; +const { updateQuery } = url; /** * Handles changeFilterName action. @@ -135,7 +135,8 @@ function onSelectBucket(state, { payload }) { function onSelectSavedFilter(state, { payload }) { const { isForReviewOpportunities } = state.savedFilters[payload].filter; updateQuery({ - bucket: isForReviewOpportunities ? BUCKETS.SAVED_REVIEW_OPPORTUNITIES_FILTER : undefined, + bucket: isForReviewOpportunities + ? BUCKETS.SAVED_REVIEW_OPPORTUNITIES_FILTER : undefined, }); return { ...state, @@ -164,7 +165,7 @@ function onUpdateSavedFilter(state, action) { } function create(initialState = {}) { - const a = actions.challengeListing.sidebar; + const a = actions.challengeListingFrontend.sidebar; return handleActions({ [a.changeFilterName]: onChangeFilterName, [a.deleteSavedFilter]: onDeleteSavedFilter, diff --git a/src/shared/reducers/index.js b/src/shared/reducers/index.js index 769355cbc7..821fae67ba 100644 --- a/src/shared/reducers/index.js +++ b/src/shared/reducers/index.js @@ -24,13 +24,13 @@ import { getAuthTokens } from 'utils/tc'; import contentful from './contentful'; import topcoderHeader from './topcoder_header'; import rss from './rss'; -import { factory as challengeListingFactory } from './challenge-listing'; import { factory as examplesFactory } from './examples'; import { factory as pageFactory } from './page'; import { factory as tcCommunitiesFactory } from './tc-communities'; import { factory as leaderboardFactory } from './leaderboard'; import { factory as scoreboardFactory } from './tco/scoreboard'; import { factory as termsFactory } from './terms'; +import { factory as challengeListingFrontendFactory } from './challenge-listing'; /** * Given HTTP request, generates options for SSR by topcoder-react-lib's reducer @@ -108,13 +108,13 @@ function generateSsrOptions(req) { export function factory(req) { return redux.resolveReducers({ standard: reducerFactory(req && generateSsrOptions(req)), - challengeListing: challengeListingFactory(req), examples: examplesFactory(req), tcCommunities: tcCommunitiesFactory(req), leaderboard: leaderboardFactory(req), scoreboard: scoreboardFactory(req), terms: termsFactory(req), page: pageFactory(req), + challengeListingFrontend: challengeListingFrontendFactory(req), }).then(resolvedReducers => redux.combineReducers((state) => { const res = { ...state }; if (req) { diff --git a/src/shared/reducers/page/challenge-details.js b/src/shared/reducers/page/challenge-details.js index 46ac60cbef..9b2d4a2c77 100644 --- a/src/shared/reducers/page/challenge-details.js +++ b/src/shared/reducers/page/challenge-details.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import actions, { SPECS_TAB_STATES } from 'actions/page/challenge-details'; import { handleActions } from 'redux-actions'; -import { updateQuery } from 'utils/url'; +import { url } from 'topcoder-react-lib'; /** * Handles challengeActions.toggleCheckpointFeedback action. @@ -30,7 +30,7 @@ function onToggleCheckpointFeedback(state, action) { * @return {Object} */ function onSelectTab(state, { payload }) { - updateQuery({ tab: payload }); + url.updateQuery({ tab: payload }); return { ...state, selectedTab: payload }; } diff --git a/src/shared/routes/Communities/Veterans/Routes.jsx b/src/shared/routes/Communities/Veterans/Routes.jsx index a76d6e48c1..294f6cde82 100644 --- a/src/shared/routes/Communities/Veterans/Routes.jsx +++ b/src/shared/routes/Communities/Veterans/Routes.jsx @@ -9,8 +9,7 @@ import Footer from 'components/tc-communities/communities/veterans/Footer'; import Header from 'containers/tc-communities/Header'; import Home from 'components/tc-communities/communities/veterans/Home'; import imageTextStyle from 'components/tc-communities/communities/veterans/themes/image-text.scss'; -import PreListingMsg from - 'components/tc-communities/communities/veterans/PreListingMsg'; +import PreListingMsg from 'components/tc-communities/communities/veterans/PreListingMsg'; import Learn from 'components/tc-communities/communities/veterans/Learn'; import PT from 'prop-types'; import React from 'react'; @@ -19,11 +18,12 @@ import SubmissionManagement from 'routes/SubmissionManagement'; import TermsDetail from 'routes/TermsDetail'; import { ThemeProvider } from 'react-css-super-themr'; import { Route, Switch } from 'react-router-dom'; -import { BUCKETS, registerBucket } from 'utils/challenge-listing/buckets'; -import { SORTS } from 'utils/challenge-listing/sort'; - +import { challenge as challengeUtil } from 'topcoder-react-lib'; import Leaderboard from '../Leaderboard'; +const { BUCKETS, registerBucket } = challengeUtil.buckets; +const { SORTS } = challengeUtil.sort; + export default function Veterans({ base, member, meta }) { const ID = 'ACTIVE_VETERANS_CHALLENGES'; if (!BUCKETS[ID]) { diff --git a/src/shared/utils/challenge-listing/buckets.js b/src/shared/utils/challenge-listing/buckets.js deleted file mode 100644 index 8914c9a00d..0000000000 --- a/src/shared/utils/challenge-listing/buckets.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Standard challenge buckets, selectable by the sidebar. - */ - -import _ from 'lodash'; -import { SORTS } from './sort'; - -export const BUCKETS = { - ALL: 'all', - MY: 'my', - OPEN_FOR_REGISTRATION: 'openForRegistration', - ONGOING: 'ongoing', - PAST: 'past', - SAVED_FILTER: 'saved-filter', - UPCOMING: 'upcoming', - REVIEW_OPPORTUNITIES: 'reviewOpportunities', - SAVED_REVIEW_OPPORTUNITIES_FILTER: 'savedReviewOpportunitiesFilter', -}; - -const BUCKET_DATA = { - [BUCKETS.ALL]: { - filter: { - started: true, - status: ['ACTIVE'], - }, - hideCount: false, - name: 'All Challenges', - sorts: [], - }, - [BUCKETS.MY]: { - filter: { - started: true, - status: ['ACTIVE'], - // users: [userHandle], - }, - hideCount: false, - name: 'My Challenges', - sorts: [ - SORTS.MOST_RECENT, - SORTS.TIME_TO_SUBMIT, - SORTS.NUM_REGISTRANTS, - SORTS.NUM_SUBMISSIONS, - SORTS.PRIZE_HIGH_TO_LOW, - SORTS.TITLE_A_TO_Z, - ], - }, - [BUCKETS.OPEN_FOR_REGISTRATION]: { - filter: { - registrationOpen: true, - started: true, - status: ['ACTIVE'], - }, - hideCount: false, - name: 'Open for registration', - sorts: [ - SORTS.MOST_RECENT, - SORTS.TIME_TO_REGISTER, - SORTS.TIME_TO_SUBMIT, - SORTS.NUM_REGISTRANTS, - SORTS.NUM_SUBMISSIONS, - SORTS.PRIZE_HIGH_TO_LOW, - SORTS.TITLE_A_TO_Z, - ], - }, - [BUCKETS.ONGOING]: { - filter: { - registrationOpen: false, - started: true, - status: ['ACTIVE'], - }, - hideCount: false, - name: 'Ongoing challenges', - sorts: [ - SORTS.MOST_RECENT, - SORTS.CURRENT_PHASE, - SORTS.TITLE_A_TO_Z, - SORTS.PRIZE_HIGH_TO_LOW, - ], - }, - [BUCKETS.UPCOMING]: { - filter: { - upcoming: true, - }, - hideCount: true, - name: 'Upcoming challenges', - sorts: [ - SORTS.MOST_RECENT, - SORTS.PRIZE_HIGH_TO_LOW, - SORTS.TITLE_A_TO_Z, - ], - }, - [BUCKETS.PAST]: { - filter: { status: ['COMPLETED', 'PAST'] }, - hideCount: true, - name: 'Past challenges', - sorts: [ - SORTS.MOST_RECENT, - SORTS.PRIZE_HIGH_TO_LOW, - SORTS.TITLE_A_TO_Z, - ], - }, - [BUCKETS.REVIEW_OPPORTUNITIES]: { - filter: {}, - hideCount: true, - name: 'Open for review', - sorts: [ - SORTS.REVIEW_OPPORTUNITIES_START_DATE, - SORTS.REVIEW_OPPORTUNITIES_PAYMENT, - SORTS.REVIEW_OPPORTUNITIES_TITLE_A_TO_Z, - ], - }, - [BUCKETS.SAVED_REVIEW_OPPORTUNITIES_FILTER]: { - filter: {}, - sorts: [ - SORTS.REVIEW_OPPORTUNITIES_START_DATE, - SORTS.REVIEW_OPPORTUNITIES_PAYMENT, - SORTS.REVIEW_OPPORTUNITIES_TITLE_A_TO_Z, - ], - }, -}; - -/** - * Returns configuration of all possible challenge buckets. - * @param {String} userHandle Handle of the authenticated - * user to filter out My Challenges. - */ -export function getBuckets(userHandle) { - const res = _.cloneDeep(BUCKET_DATA); - res[BUCKETS.MY].filter.users = [userHandle]; - return res; -} - -/** - * Tests if a given bucket is of any of the Review Opportunities types - * @param {String} bucket The bucket in question - * @return {Boolean} True if the bucket contains Review Opportunities - */ -export const isReviewOpportunitiesBucket = bucket => ( - bucket === BUCKETS.REVIEW_OPPORTUNITIES || bucket === BUCKETS.SAVED_REVIEW_OPPORTUNITIES_FILTER); - -/** - * Registers a new bucket. - * @param {String} id - * @param {Object} bucket - */ -export function registerBucket(id, bucket) { - if (BUCKET_DATA[id]) { - throw new Error('Bucket ID clush with an existing bucket'); - } - BUCKETS[id] = id; - BUCKET_DATA[id] = bucket; -} - -export default undefined; diff --git a/src/shared/utils/challenge-listing/sort.js b/src/shared/utils/challenge-listing/sort.js deleted file mode 100644 index 407ef05672..0000000000 --- a/src/shared/utils/challenge-listing/sort.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Collection of compare function to sort challenges in different ways. - */ - -import moment from 'moment'; -import { sumBy } from 'lodash'; - -export const SORTS = { - CURRENT_PHASE: 'current-phase', - MOST_RECENT: 'most-recent', - NUM_REGISTRANTS: 'num-registrants', - NUM_SUBMISSIONS: 'num-submissions', - PRIZE_HIGH_TO_LOW: 'prize-high-to-low', - TIME_TO_REGISTER: 'time-to-register', - TIME_TO_SUBMIT: 'time-to-submit', - TITLE_A_TO_Z: 'title-a-to-z', - 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', -}; - -export default { - [SORTS.CURRENT_PHASE]: { - func: (a, b) => a.status.localeCompare(b.status), - name: 'Current phase', - }, - [SORTS.MOST_RECENT]: { - func: (a, b) => moment(b.registrationStartDate).diff(a.registrationStartDate), - name: 'Most recent', - }, - [SORTS.NUM_REGISTRANTS]: { - func: (a, b) => b.numRegistrants - a.numRegistrants, - name: '# of registrants', - }, - [SORTS.NUM_SUBMISSIONS]: { - func: (a, b) => b.numSubmissions - a.numSubmissions, - name: '# of submissions', - }, - [SORTS.PRIZE_HIGH_TO_LOW]: { - func: (a, b) => b.totalPrize - a.totalPrize, - name: 'Prize high to low', - }, - [SORTS.TIME_TO_REGISTER]: { - func: (a, b) => moment(a.registrationEndDate || a.submissionEndDate) - .diff(b.registrationEndDate || b.submissionEndDate), - name: 'Time to register', - }, - [SORTS.TIME_TO_SUBMIT]: { - func: (a, b) => { - function nextSubEndDate(o) { - if (o.checkpointSubmissionEndDate && moment(o.checkpointSubmissionEndDate).isAfter()) { - return o.checkpointSubmissionEndDate; - } - return o.submissionEndDate; - } - - const aDate = nextSubEndDate(a); - const bDate = nextSubEndDate(b); - - if (moment(aDate).isBefore()) return 1; - if (moment(bDate).isBefore()) return -1; - - return moment(aDate).diff(bDate); - }, - name: 'Time to submit', - }, - [SORTS.TITLE_A_TO_Z]: { - func: (a, b) => a.name.localeCompare(b.name), - name: 'Title A-Z', - }, - [SORTS.REVIEW_OPPORTUNITIES_TITLE_A_TO_Z]: { - func: (a, b) => a.challenge.title.localeCompare(b.challenge.title), - name: 'Title A-Z', - }, - [SORTS.REVIEW_OPPORTUNITIES_PAYMENT]: { - func: (a, b) => sumBy(b.payments, 'payment') - sumBy(a.payments, 'payment'), - name: 'Payment', - }, - [SORTS.REVIEW_OPPORTUNITIES_START_DATE]: { - // This will implicitly use moment#valueOf - func: (a, b) => moment(a.startDate) - moment(b.startDate), - name: 'Review start date', - }, -}; diff --git a/src/shared/utils/url.js b/src/shared/utils/url.js deleted file mode 100644 index 73154aa74a..0000000000 --- a/src/shared/utils/url.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Various URL-related functions. - */ - -/* global window */ - -import _ from 'lodash'; -import qs from 'qs'; -import { isomorphy } from 'topcoder-react-utils'; - -/** - * If executed client-side (determined in this case by the presence of global - * window object), this function updates query section of URL; otherwise does - * nothing. - * @param {Object} update Specifies the update to make. Current query will be - * parsed into JS object, then update will be merged into that object, and the - * result will be pushed back to the query section of URL. I.e. to unset some - * field of the query, that field should be explicitely mentioned inside - * 'update' as undefined. - */ -export function updateQuery(update) { - if (isomorphy.isServerSide()) return; - - let query = qs.parse(window.location.search.slice(1)); - - /* _.merge won't work here, because it just ignores the fields explicitely - * set as undefined in the objects to be merged, rather than deleting such - * fields in the target object. */ - _.forIn(update, (value, key) => { - if (_.isUndefined(value)) delete query[key]; - else query[key] = value; - }); - query = `?${qs.stringify(query, { encodeValuesOnly: true })}`; - window.history.replaceState(window.history.state, '', query); -} - -/** - * Cleans/removes trailing slash from url - * - * @param {String} url The url to clean - * @return {String} - */ -export function removeTrailingSlash(url) { - return url.charAt(url.length - 1) === '/' - ? url.slice(0, -1) - : url; -} - -export default undefined; From d144471c98e3830766c49fd76c80c6e970eda886 Mon Sep 17 00:00:00 2001 From: Huan Li Date: Sun, 28 Apr 2019 11:39:02 +0800 Subject: [PATCH 3/4] Keep challenge util reference name consistent --- .../Dashboard/CurrentActivity/Challenges/index.jsx | 4 ++-- src/shared/components/Dashboard/CurrentActivity/index.jsx | 4 ++-- .../challenge-listing/Filters/ChallengeFilters.jsx | 4 ++-- .../challenge-listing/Filters/FiltersPanel/index.jsx | 4 ++-- .../Listing/ReviewOpportunityBucket/index.jsx | 4 ++-- .../Sidebar/BucketSelector/Bucket/index.jsx | 4 ++-- .../challenge-listing/Sidebar/BucketSelector/index.jsx | 6 +++--- src/shared/containers/challenge-detail/index.jsx | 6 +++--- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/shared/components/Dashboard/CurrentActivity/Challenges/index.jsx b/src/shared/components/Dashboard/CurrentActivity/Challenges/index.jsx index a98d7bd127..d333654880 100644 --- a/src/shared/components/Dashboard/CurrentActivity/Challenges/index.jsx +++ b/src/shared/components/Dashboard/CurrentActivity/Challenges/index.jsx @@ -6,14 +6,14 @@ import React from 'react'; import Sticky from 'react-stickynode'; import { config, Link } from 'topcoder-react-utils'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import ChallengeCard from './ChallengeCard'; import ChallengeFilter from '../ChallengeFilter'; import style from './style.scss'; -const Filter = challengeUtils.filter; +const Filter = challengeUtil.filter; export default function Challenges({ challengeFilter, diff --git a/src/shared/components/Dashboard/CurrentActivity/index.jsx b/src/shared/components/Dashboard/CurrentActivity/index.jsx index c1b0cd1890..70b8225058 100644 --- a/src/shared/components/Dashboard/CurrentActivity/index.jsx +++ b/src/shared/components/Dashboard/CurrentActivity/index.jsx @@ -5,7 +5,7 @@ import PT from 'prop-types'; import _ from 'lodash'; import { TABS } from 'actions/page/dashboard'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import Challenges from './Challenges'; import Communities from './Communities'; @@ -14,7 +14,7 @@ import Srms from './Srms'; import './styles.scss'; -const Filter = challengeUtils.filter; +const Filter = challengeUtil.filter; /* eslint-disable react/no-unused-state */ diff --git a/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx b/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx index 435185b8b5..33c9bc8c7c 100644 --- a/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx +++ b/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx @@ -6,7 +6,7 @@ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; import SwitchWithLabel from 'components/SwitchWithLabel'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import { COMPETITION_TRACKS as TRACKS } from 'utils/tc'; import ChallengeSearchBar from './ChallengeSearchBar'; @@ -18,7 +18,7 @@ import FiltersCardsType from './FiltersCardsType'; import './ChallengeFilters.scss'; -const Filter = challengeUtils.filter; +const Filter = challengeUtil.filter; export default function ChallengeFilters({ communityFilters, diff --git a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx index b3201221b6..43682e0297 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 { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import React from 'react'; import PT from 'prop-types'; import Select from 'components/Select'; @@ -37,7 +37,7 @@ import DateRangePicker from '../DateRangePicker'; import style from './style.scss'; import UiSimpleRemove from '../../Icons/ui-simple-remove.svg'; -const Filter = challengeUtils.filter; +const Filter = challengeUtil.filter; export default function FiltersPanel({ communityFilters, diff --git a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx index 43243f35af..5d480cc1fe 100644 --- a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx @@ -5,13 +5,13 @@ import PT from 'prop-types'; import React from 'react'; import SortingSelectBar from 'components/SortingSelectBar'; import Waypoint from 'react-waypoint'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import CardPlaceholder from '../../placeholders/ChallengeCard'; import ReviewOpportunityCard from '../../ReviewOpportunityCard'; import './style.scss'; -const { SORTS_DATA } = challengeUtils.sort; +const { SORTS_DATA } = challengeUtil.sort; const NO_RESULTS_MESSAGE = 'There are no review opportunities available'; diff --git a/src/shared/components/challenge-listing/Sidebar/BucketSelector/Bucket/index.jsx b/src/shared/components/challenge-listing/Sidebar/BucketSelector/Bucket/index.jsx index 57760bd40b..2067e448b7 100644 --- a/src/shared/components/challenge-listing/Sidebar/BucketSelector/Bucket/index.jsx +++ b/src/shared/components/challenge-listing/Sidebar/BucketSelector/Bucket/index.jsx @@ -3,13 +3,13 @@ */ import _ from 'lodash'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import PT from 'prop-types'; import { connect } from 'react-redux'; import React from 'react'; import './style.scss'; -const Filter = challengeUtils.filter; +const Filter = challengeUtil.filter; function Bucket({ active, diff --git a/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx b/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx index 28dffee403..e421db5a8c 100644 --- a/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx +++ b/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx @@ -6,14 +6,14 @@ import PT from 'prop-types'; import React from 'react'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { challenge as challengeUtil } from 'topcoder-react-lib'; import Bucket from './Bucket'; import './style.scss'; -const Filter = challengeUtils.filter; -const Buckets = challengeUtils.buckets; +const Filter = challengeUtil.filter; +const Buckets = challengeUtil.buckets; const RSS_LINK = 'http://feeds.topcoder.com/challenges/feed?list=active&contestType=all'; diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index ea6f0545b6..f593421b6c 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -28,7 +28,7 @@ import challengeDetailsActions, { TABS as DETAIL_TABS } from 'actions/page/challenge-details'; import { CHALLENGE_PHASE_TYPES, COMPETITION_TRACKS_V3, SUBTRACKS } from 'utils/tc'; import { config, MetaTags } from 'topcoder-react-utils'; -import { actions, challenges } from 'topcoder-react-lib'; +import { actions, challenge as challengeUtil } from 'topcoder-react-lib'; import ogWireframe from '../../../assets/images/open-graph/challenges/01-wireframe.jpg'; @@ -56,7 +56,7 @@ import ogImage from '../../../assets/images/og_image.jpg'; import './styles.scss'; -const Buckets = challenges.buckets; +const { BUCKETS } = challengeUtil.buckets; /* Holds various time ranges in milliseconds. */ const MIN = 60 * 1000; @@ -543,7 +543,7 @@ const mapDispatchToProps = (dispatch) => { const cls = challengeListingSidebarActions.challengeListing.sidebar; const cl = actions.challenge; dispatch(cl.setFilter(filter)); - dispatch(cls.selectBucket(Buckets.BUCKETS.ALL)); + dispatch(cls.selectBucket(BUCKETS.ALL)); }, setSpecsTabState: state => dispatch(pageActions.page.challengeDetails.setSpecsTabState(state)), unregisterFromChallenge: (auth, challengeId) => { From 5084cc560200b01f403add439b7f4548d3a6fa42 Mon Sep 17 00:00:00 2001 From: Huan Li Date: Sun, 28 Apr 2019 12:12:37 +0800 Subject: [PATCH 4/4] Check member's each external data before render component --- src/shared/components/ProfilePage/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/ProfilePage/index.jsx b/src/shared/components/ProfilePage/index.jsx index 2ff15bf9a5..c8990f35ce 100644 --- a/src/shared/components/ProfilePage/index.jsx +++ b/src/shared/components/ProfilePage/index.jsx @@ -267,7 +267,7 @@ On The Web
{ - externals.map(external => ( + externals.map(external => !_.isEmpty(external.data) && (