diff --git a/__tests__/__snapshots__/index.js.snap b/__tests__/__snapshots__/index.js.snap index 0542a66c..e7ee51bc 100644 --- a/__tests__/__snapshots__/index.js.snap +++ b/__tests__/__snapshots__/index.js.snap @@ -29,6 +29,31 @@ Object { "updateChallengeDone": [Function], "updateChallengeInit": [Function], }, + "challengeListing": Object { + "dropChallenges": [Function], + "expandTag": [Function], + "getActiveChallengesDone": [Function], + "getActiveChallengesInit": [Function], + "getAllActiveChallengesDone": [Function], + "getAllActiveChallengesInit": [Function], + "getChallengeSubtracksDone": [Function], + "getChallengeSubtracksInit": [Function], + "getChallengeTagsDone": [Function], + "getChallengeTagsInit": [Function], + "getMoreChallenges": [Function], + "getPastChallengesDone": [Function], + "getPastChallengesInit": [Function], + "getRestActiveChallengesDone": [Function], + "getRestActiveChallengesInit": [Function], + "getReviewOpportunitiesDone": [Function], + "getReviewOpportunitiesInit": [Function], + "getSrmsDone": [Function], + "getSrmsInit": [Function], + "selectCommunity": [Function], + "setDatepickerStatus": [Function], + "setFilter": [Function], + "setSort": [Function], + }, "direct": Object { "dropAll": [Function], "getProjectDetailsDone": [Function], @@ -173,13 +198,145 @@ Object { }, }, "challenge": Object { + "buckets": Object { + "BUCKETS": Object { + "ALL": "all", + "MY": "my", + "ONGOING": "ongoing", + "OPEN_FOR_REGISTRATION": "openForRegistration", + "PAST": "past", + "REVIEW_OPPORTUNITIES": "reviewOpportunities", + "SAVED_FILTER": "saved-filter", + "SAVED_REVIEW_OPPORTUNITIES_FILTER": "savedReviewOpportunitiesFilter", + "UPCOMING": "upcoming", + }, + "BUCKET_DATA": Object { + "all": Object { + "filter": Object { + "started": true, + "status": Array [ + "ACTIVE", + ], + }, + "hideCount": false, + "name": "All Challenges", + "sorts": Array [], + }, + "my": Object { + "filter": Object { + "started": true, + "status": Array [ + "ACTIVE", + ], + }, + "hideCount": false, + "name": "My Challenges", + "sorts": Array [ + "most-recent", + "time-to-submit", + "num-registrants", + "num-submissions", + "prize-high-to-low", + "title-a-to-z", + ], + }, + "ongoing": Object { + "filter": Object { + "registrationOpen": false, + "started": true, + "status": Array [ + "ACTIVE", + ], + }, + "hideCount": false, + "name": "Ongoing challenges", + "sorts": Array [ + "most-recent", + "current-phase", + "title-a-to-z", + "prize-high-to-low", + ], + }, + "openForRegistration": Object { + "filter": Object { + "registrationOpen": true, + "started": true, + "status": Array [ + "ACTIVE", + ], + }, + "hideCount": false, + "name": "Open for registration", + "sorts": Array [ + "most-recent", + "time-to-register", + "time-to-submit", + "num-registrants", + "num-submissions", + "prize-high-to-low", + "title-a-to-z", + ], + }, + "past": Object { + "filter": Object { + "status": Array [ + "COMPLETED", + "PAST", + ], + }, + "hideCount": true, + "name": "Past challenges", + "sorts": Array [ + "most-recent", + "prize-high-to-low", + "title-a-to-z", + ], + }, + "reviewOpportunities": Object { + "filter": Object {}, + "hideCount": true, + "name": "Open for review", + "sorts": Array [ + "review-opportunities-start-date", + "review-opportunities-payment", + "review-opportunities-title-a-to-z", + ], + }, + "savedReviewOpportunitiesFilter": Object { + "filter": Object {}, + "sorts": Array [ + "review-opportunities-start-date", + "review-opportunities-payment", + "review-opportunities-title-a-to-z", + ], + }, + "upcoming": Object { + "filter": Object { + "upcoming": true, + }, + "hideCount": true, + "name": "Upcoming challenges", + "sorts": Array [ + "most-recent", + "prize-high-to-low", + "title-a-to-z", + ], + }, + }, + "default": undefined, + "getBuckets": [Function], + "isReviewOpportunitiesBucket": [Function], + "registerBucket": [Function], + }, "filter": Object { "addTrack": [Function], "combine": [Function], "default": undefined, + "filterByDate": [Function], "getFilterFunction": [Function], "getReviewOpportunitiesFilterFunction": [Function], "mapToBackend": [Function], + "newMeta": [Function], "removeTrack": [Function], "setEndDate": [Function], "setReviewOpportunityType": [Function], @@ -188,6 +345,67 @@ Object { "setTags": [Function], "setText": [Function], }, + "sort": Object { + "SORTS": Object { + "CURRENT_PHASE": "current-phase", + "MOST_RECENT": "most-recent", + "NUM_REGISTRANTS": "num-registrants", + "NUM_SUBMISSIONS": "num-submissions", + "PRIZE_HIGH_TO_LOW": "prize-high-to-low", + "REVIEW_OPPORTUNITIES_PAYMENT": "review-opportunities-payment", + "REVIEW_OPPORTUNITIES_START_DATE": "review-opportunities-start-date", + "REVIEW_OPPORTUNITIES_TITLE_A_TO_Z": "review-opportunities-title-a-to-z", + "TIME_TO_REGISTER": "time-to-register", + "TIME_TO_SUBMIT": "time-to-submit", + "TITLE_A_TO_Z": "title-a-to-z", + }, + "SORTS_DATA": Object { + "current-phase": Object { + "func": [Function], + "name": "Current phase", + }, + "most-recent": Object { + "func": [Function], + "name": "Most recent", + }, + "num-registrants": Object { + "func": [Function], + "name": "# of registrants", + }, + "num-submissions": Object { + "func": [Function], + "name": "# of submissions", + }, + "prize-high-to-low": Object { + "func": [Function], + "name": "Prize high to low", + }, + "review-opportunities-payment": Object { + "func": [Function], + "name": "Payment", + }, + "review-opportunities-start-date": Object { + "func": [Function], + "name": "Review start date", + }, + "review-opportunities-title-a-to-z": Object { + "func": [Function], + "name": "Title A-Z", + }, + "time-to-register": Object { + "func": [Function], + "name": "Time to register", + }, + "time-to-submit": Object { + "func": [Function], + "name": "Time to submit", + }, + "title-a-to-z": Object { + "func": [Function], + "name": "Title A-Z", + }, + }, + }, }, "errors": Object { "ERROR_ICON_TYPES": Object { @@ -227,6 +445,7 @@ Object { "reducers": Object { "auth": [Function], "challenge": [Function], + "challengeListing": [Function], "direct": [Function], "errors": [Function], "groups": [Function], @@ -321,11 +540,17 @@ Object { }, "getApiResponsePayload": [Function], "getLookerApiResponsePayload": [Function], + "processSRM": [Function], }, "time": Object { "default": undefined, "delay": [Function], "formatDuration": [Function], }, + "url": Object { + "default": undefined, + "removeTrailingSlash": [Function], + "updateQuery": [Function], + }, } `; diff --git a/docs/actions.challenge-listing.md b/docs/actions.challenge-listing.md new file mode 100644 index 00000000..85843947 --- /dev/null +++ b/docs/actions.challenge-listing.md @@ -0,0 +1,271 @@ + + +## actions.challenge-listing +Actions related to Topcoder challenge-listing APIs. + + +* [actions.challenge-listing](#module_actions.challenge-listing) + * [.dropChallenges(bucket)](#module_actions.challenge-listing.dropChallenges) ⇒ Action + * [.getMoreChallenges(bucket)](#module_actions.challenge-listing.getMoreChallenges) ⇒ Action + * [.getAllActiveChallengesInit(uuid)](#module_actions.challenge-listing.getAllActiveChallengesInit) ⇒ Action + * [.getAllActiveChallengesDone(uuid, tokenV3)](#module_actions.challenge-listing.getAllActiveChallengesDone) ⇒ Action + * [.getActiveChallengesInit(uuid, page, frontFilter, sort, bucket)](#module_actions.challenge-listing.getActiveChallengesInit) ⇒ Action + * [.getActiveChallengesDone( + uuid, page, backendFilter, tokenV3, frontFilter, sort, bucket, +)](#module_actions.challenge-listing.getActiveChallengesDone) ⇒ Action + * [.getRestActiveChallengesInit(uuid)](#module_actions.challenge-listing.getRestActiveChallengesInit) ⇒ Action + * [.getRestActiveChallengesDone( + uuid, tokenV3, backendFilter, frontFilter, sort, bucket, +)](#module_actions.challenge-listing.getRestActiveChallengesDone) ⇒ Action + * [.getChallengeSubtracksInit()](#module_actions.challenge-listing.getChallengeSubtrackInit) ⇒ Action + * [.getChallengeSubtracksDone()](#module_actions.challenge-listing.getChallengeSubtracksDone) ⇒ Action + * [.getChallengeTagsInit()](#module_actions.challenge-listing.getChallengeTagsInit) ⇒ Action + * [.getChallengeTagsDone()](#module_actions.challenge-listing.getChallengeTagsDone) ⇒ Action + * [.getPastChallengesInit(uuid, page, frontFilter, sort)](#module_actions.challenge-listing.getPastChallengesInit) ⇒ Action + * [.getPastChallengesDone(uuid, page, filter, tokenV3, frontFilter, sort)](#module_actions.challenge-listing.getPastChallengesDone) ⇒ Action + * [.getReviewOpportunitiesInit(uuid, page, sort)](#module_actions.challenge-listing.getReviewOpportunitiesInit) ⇒ Action + * [.getReviewOpportunitiesDone(uuid, page, tokenV3, sort, frontFilter)](#module_actions.challenge-listing.getReviewOpportunitiesDone) ⇒ Action + * [.getSrmsInit(uuid)](#module_actions.challenge-listing.getSrmsInit) ⇒ Action + * [.getSrmsDone(uuid, handle, params, tokenV3)](#module_actions.challenge-listing.getSrmsDone) ⇒ Action + * [.expandTag(id)](#module_actions.challenge-listing.expandTag) ⇒ Action + * [.selectCommunity()](#module_actions.challenge-listing.selectCommunity) ⇒ Action + * [.setFilter()](#module_actions.challenge-listing.setFilter) ⇒ Action + * [.setDatepickerStatus(status)](#module_actions.challenge-listing.setDatepickerStatus) ⇒ Action + * [.setSort(bucket, sort)](#module_actions.challenge-listing.setSort) ⇒ Action + + + +### actions.challenge-listing.dropChallenges(bucket) ⇒ Action +Creates an action that drops from Redux store all challenges-list related loaded. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| bucket | String | Bucket name | + + + +### actions.challenge-listing.getMoreChallenges(bucket) ⇒ Action +Creates an action that get more challenges of bucket. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| bucket | String | Bucket name | + + + +### actions.challenge-listing.getAllActiveChallengesInit(uuid) ⇒ Action +Creates an action that signals beginning of all active challenges loading. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | + + + +### actions.challenge-listing.getAllActiveChallengesInit(uuid, tokenV3) ⇒ Action +Creates an action that loads all active challenges. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| tokenV3 | String | Topcoder v3 auth token. | + + +### actions.challenge-listing.getActiveChallengesInit(uuid, page, frontFilter, sort, bucket) ⇒ Action +Creates an action that signals beginning of active challenges of bucket loading. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| page | Number | Page number of fetch data | +| frontFilter | Object | Filter Object from Client | +| sort | String | Sort name | +| bucket | String | Bucket name | + + + +### actions.challenge-listing.getActiveChallengesDone( + uuid, page, backendFilter, tokenV3, frontFilter, sort, bucket, +) ⇒ Action +Creates an action that loads active challenges of bucket. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| page | Number | Page number of fetch data | +| backendFilter | Object | Filter Object from Backend | +| tokenV3 | String | Topcoder v3 auth token | +| frontFilter | Object | Filter Object from Client | +| sort | String | Sort name | +| bucket | String | Bucket name | + + + +### actions.challenge-listing.getRestActiveChallengesInit(uuid) ⇒ Action +Creates an action that signals beginning of rest active challenges of bucket loading. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | + + + +### actions.challenge-listing.getRestActiveChallengesDone( + uuid, tokenV3, backendFilter, frontFilter, sort, bucket, +) ⇒ Action +Creates an action that loads rest active challenges of bucket. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| tokenV3 | String | Topcoder v3 auth token | +| backendFilter | Object | Filter Object from Backend | +| frontFilter | Object | Filter Object from Client | +| sort | String | Sort name | +| bucket | String | Bucket name | + + + +### actions.challenge-listing.getChallengeSubtracksInit() ⇒ Action +Creates an action that signals beginning of challenge substrcks loading. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + + +### actions.challenge-listing.getChallengeSubtracksDone()⇒ Action +Creates an action that loads challenge substrcks. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + + +### actions.challenge-listing.getChallengeTagsInit() ⇒ Action +Creates an action that signals beginning of challenge tags loading. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + + +### actions.challenge-listing.getChallengeTagsDone()⇒ Action +Creates an action that loads challenge tags. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + + +### actions.challenge-listing.getPastChallengesInit(uuid, page, frontFilter, sort) ⇒ Action +Creates an action that signals beginning of past challenges loading. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| page | Number | Page number of fetch data | +| frontFilter | Object | Filter Object from Client | +| sort | String | Sort name | + + + +### actions.challenge-listing.getPastChallengesDone(uuid, page, filter, tokenV3, frontFilter, sort) ⇒ Action +Creates an action that loads past challenges. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| page | Number | Page number of fetch data | +| filter | Object | Filter Object from Backend | +| tokenV3 | String | Topcoder v3 auth token | +| frontFilter | Object | Filter Object from Client | +| sort | String | Sort name | + + + +### actions.challenge-listing.getReviewOpportunitiesInit(uuid, page, sort) ⇒ Action +Creates an action that signals beginning of review opportunities loading. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| page | Number | Page number of fetch data | +| sort | String | Sort name | + + + +### actions.challenge-listing.getReviewOpportunitiesDone(uuid, page, tokenV3, sort, frontFilter) ⇒ Action +Creates an action that loads review oportunites. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| page | Number | Page number of fetch data | +| tokenV3 | String | Topcoder v3 auth token | +| sort | String | Sort name | +| frontFilter | Object | Filter Object from Client | + + + + +### actions.challenge-listing.getSrmsInit(uuid) ⇒ Action +Creates an action that signals beginning of SRMs loading. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | + + +### actions.challenge-listing.getSrmsDone(uuid, handle, params, tokenV3) ⇒ Action +Creates an action that SRMs. +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| uuid | String | UUID of the operation (the same should be passed into the corresponding | +| handle | String | Topcoder member handle | +| params | Object | params of fetch data | +| tokenV3 | String | Topcoder v3 auth token | + + + +### actions.challenge-listing.expandTag(id) ⇒ Action +Creates an action that set tag id +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| id | String | Id of tag | + + + +### actions.challenge-listing.selectCommunity() ⇒ Action +Creates an action that pass community id +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + + +### actions.challenge-listing.setFilter() ⇒ Action +Creates an action that pass filter value +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + + +### actions.challenge-listing.setDatepickerStatus(status) ⇒ Action +Creates an action that set Datepicker status +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| status | Boolean | Status datapicker | + + + +### actions.challenge-listing.setSort(bucket, sort) ⇒ Action +Creates an action that set sort of bucket +**Kind**: static method of [actions.challenge-listing](#module_actions.challenge-listing) + +| Param | Type | Description | +| --- | --- | --- | +| bucket | String | Bucket name | +| sort | String | Sort name | diff --git a/docs/buckets.md b/docs/buckets.md new file mode 100644 index 00000000..80072bd3 --- /dev/null +++ b/docs/buckets.md @@ -0,0 +1,50 @@ + + +## buckets +Collection of buckets of challenge + +* [challenge_buckets](#module_challenge_buckets) + * [.BUCKETS](#module_challenge_buckets.BUCKETS) + * [.BUCKET_DATA](#module_challenge_buckets.BUCKET_DATA) + * [.getBuckets(res)](#module_challenge_buckets.getBuckets) ⇒ Promise + * [.isReviewOpportunitiesBucket(res)](#module_challenge_buckets.isReviewOpportunitiesBucket) ⇒ Promise + * [.registerBucket](#module_challenge_buckets.registerBucket) + + +### challenge_buckets.BUCKETS +Bucket types +**Kind**: static constant of [challenge_buckets](#module_challenge_buckets) + + + +### challenge_buckets.BUCKET_DATA +The data of bucket +**Kind**: static constant of [challenge_buckets](#module_challenge_buckets) + + + +### challenge_buckets.getBuckets(userHandle) ⇒ Promise +Returns configuration of all possible challenge buckets. +**Kind**: static method of [challenge_buckets](#module_challenge_buckets) +**Returns**: Promise - Resolves to the payload. + +| Param | Type | +| --- | --- | +| res | Object | + + + +### challenge_buckets.isReviewOpportunitiesBucket(bucket) ⇒ Promise +Tests if a given bucket is of any of the Review Opportunities types +**Kind**: static method of [challenge_buckets](#module_challenge_buckets) +**Returns**: Promise - Resolves to the payload. + +| Param | Type | +| --- | --- | +| res | Boolean | + + +### challenge_buckets.registerBucket(id, bucket) ⇒ Promise +Registers a new bucket. +**Kind**: static method of [challenge_buckets](#module_challenge_buckets) + diff --git a/docs/index.md b/docs/index.md index ec065107..185c46d5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,6 +9,11 @@ actions.challenge

Actions related to Topcoder challenges APIs.

+
+
+actions.challenge-listing
+

Actions related to Topcoder challenge-listing APIs.

+
actions.direct

Actions related to Direct API: access to projects, billing accounts, @@ -71,6 +76,11 @@ actions and reducer; thus, this module.

Reducer for actions.challenge actions.

State segment managed by this reducer has the following strcuture:

+
+
+reducers.challenge-listing
+

Reducer for actions.challenge-listing actions.

+
reducers.direct

Reducer for handling the results of Direct-related actions.

@@ -275,4 +285,19 @@ the proxy will forward them to the service only if LOG_ENTRIES_TOKEN is set).

Utility functions for time/date related stuff

- +
+sort
+

Collection of challenge sort.

+
+
+
+tc
+

Collection of challenge buckets.

+
+
+
+
+url
+

Collection of url function.

+
+
diff --git a/docs/reducers.challenge-listing.md b/docs/reducers.challenge-listing.md new file mode 100644 index 00000000..ff80e6f4 --- /dev/null +++ b/docs/reducers.challenge-listing.md @@ -0,0 +1,310 @@ + + +## reducers.challenge-listing +Reducer for [actions.challenge-listing](#module_actions.challenge-listing) actions. + +State segment managed by this reducer has the following strcuture: + +**Todo** + +- [ ] Document the structure. + + +* [reducers.challenge-listing](#module_reducers.challenge-listing) + * _static_ + * [.default](#module_reducers.challenge-listing.default) + * [.factory(options)](#module_reducers.challenge-listing.factory) ⇒ Promise + * _inner_ + * [~dropChallenges(state, action)](#module_reducers.challenge-listing..dropChallenges) ⇒ Object + * [~getMoreChallenges(state, action)](#module_reducers.challenge-listing..getMoreChallenges) ⇒ Object + * [~expandTag(state, action)](#module_reducers.challenge-listing..expandTag) ⇒ Object + * [~getAllActiveChallengesInit(state, action)](#module_reducers.challenge-listing..onGetAllActiveChallengesInit) ⇒ Object + * [~getAllActiveChallengesDone(state, action)](#module_reducers.challenge-listing..onGetAllActiveChallengesDone) ⇒ Object + * [~getActiveChallengesInit(state, action)](#module_reducers.challenge-listing..getActiveChallengesInit) ⇒ Object + * [~getActiveChallengesDone(state, action)](#module_reducers.challenge-listing..getActiveChallengesDone) ⇒ Object + * [~getRestActiveChallengesInit(state, action)](#module_reducers.challenge-listing..getRestActiveChallengesInit) ⇒ Object + * [~getRestActiveChallengesDone(state, action)](#module_reducers.challenge-listing..getRestActiveChallengesDone) ⇒ Object + * [~getChallengeSubtracksInit()](#module_reducers.challenge-listing..getChallengeSubtracksInit) ⇒ Object + * [~getChallengeSubtracksDone(state, action)](#module_reducers.challenge-listing..getChallengeSubtracksDone) ⇒ Object + * [~getChallengeTagsInit()](#module_reducers.challenge-listing..getChallengeTagsInit) ⇒ Object + * [~getChallengeTagsDone(state, action)](#module_reducers.challenge-listing..getChallengeTagsDone) ⇒ Object + * [~getPastChallengesInit(state, action)](#module_reducers.challenge-listing..getPastChallengesInit) ⇒ Object + * [~getReviewOpportunitiesInit(state, action)](#module_reducers.challenge-listing..getReviewOpportunitiesInit) ⇒ Object + * [~getReviewOpportunitiesDone(state, action)](#module_reducers.challenge-listing..getReviewOpportunitiesDone) ⇒ Object + * [~getSrmsInit(state, action)](#module_reducers.challenge-listing..getSrmsInit) ⇒ Object + * [~getSrmsDone(state, action)](#module_reducers.challenge-listing..getSrmsDone) ⇒ Object + * [~selectCommunity(state, action)](#module_reducers.challenge-listing..selectCommunity) ⇒ Object + * [~setFilter(state, action)](#module_reducers.challenge-listing..setFilter) ⇒ Object + * [~setSort(state, action)](#module_reducers.challenge-listing..setSort) ⇒ Object + * [~setDatePickerStatus(state, action)](#module_reducers.challenge-listing..setDatePickerStatus) ⇒ Object + * [~create(initialState)](#module_reducers.challenge..create) ⇒ function + + +### reducers.challenge-listing.default +Reducer with default intial state. +**Kind**: static property of [reducers.challenge-listing](#module_reducers.challenge-listing) + + +### reducers.challenge-listing.factory() ⇒ Promise +Factory which creates a new reducer with its initial state tailored to the +given options object, if specified (for server-side rendering). If options +object is not specified, it creates just the default reducer. Accepted options are: + +**Kind**: static method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Resolves**: Function(state, action): state New reducer. + + +### reducers.challenge-listing~dropChallenges(state, action) ⇒ Object +Handles CHALLENGE_LISTING/DROP_CHALLENGES action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getMoreChallenges(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_MORE_CHALLENGES action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~expandTag(state, action) ⇒ Object +Handles CHALLENGE_LISTING/EXPAND_TAG action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getAllActiveChallengesInit(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_ALL_ACTIVE_CHALLENGES_INIT action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getAllActiveChallengesDone(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_ALL_ACTIVE_CHALLENGES_DONE action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getActiveChallengesInit(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_ACTIVE_CHALLENGES_INIT action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getActiveChallengesDone(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_ACTIVE_CHALLENGES_DONE action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getRestActiveChallengesInit(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_REST_ACTIVE_CHALLENGES_INIT action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getRestActiveChallengesDone(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_REST_ACTIVE_CHALLENGES_DONE action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getChallengeSubtracksInit() ⇒ Object +Handles CHALLENGE_LISTING/GET_CHALLENGE_SUBTRACKS_INIT action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + + +### reducers.challenge-listing~getChallengeSubtracksDone(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_CHALLENGE_SUBTRACKS_DONE action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getChallengeTagsInit() ⇒ Object +Handles CHALLENGE_LISTING/GET_CHALLENGE_TAGS_INIT action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + + +### reducers.challenge-listing~getChallengeTagsDone(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_CHALLENGE_TAGS_DONE action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getPastChallengesInit(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_PAST_CHALLENGES_INIT action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getPastChallengesDone(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_PAST_CHALLENGES_DONE action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| state | Object | +| action | Object | + + +### reducers.challenge-listing~getReviewOpportunitiesInit(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_REVIEW_OPPORTUNITIES_INIT action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| uuid | Object | +| action | Object | + + +### reducers.challenge-listing~getReviewOpportunitiesDone(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_REVIEW_OPPORTUNITIES_DONE action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| uuid | Object | +| action | Object | + + +### reducers.challenge-listing~getSrmsInit(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_SRMS_INIT action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| uuid | Object | +| action | Object | + + +### reducers.challenge-listing~getSrmsDone(state, action) ⇒ Object +Handles CHALLENGE_LISTING/GET_SRMS_DONE action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| uuid | Object | +| action | Object | + + + +### reducers.challenge-listing~selectCommunity(state, action) ⇒ Object +Handles CHALLENGE_LISTING/SELECT_COMMUNITY action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| uuid | Object | +| action | Object | + + +### reducers.challenge-listing~setFilter(state, action) ⇒ Object +Handles CHALLENGE_LISTING/SET_FILTER action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| uuid | Object | +| action | Object | + + +### reducers.challenge-listing~setDatePickerStatus(state, action) ⇒ Object +Handles CHALLENGE_LISTING/SET_DATEPICKER_STATUS action. +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) + +**Returns**: Object - New state + +| Param | Type | +| --- | --- | +| uuid | Object | +| action | Object | + + + + +### reducers.challenge-listing~create(initialState) ⇒ function +Creates a new Challenge-listing reducer with the specified initial state. + +**Kind**: inner method of [reducers.challenge-listing](#module_reducers.challenge-listing) +**Returns**: function - Challenge-listing reducer. + +| Param | Type | Description | +| --- | --- | --- | +| initialState | Object | Optional. Initial state. | + diff --git a/docs/sort.md b/docs/sort.md new file mode 100644 index 00000000..a7f21d61 --- /dev/null +++ b/docs/sort.md @@ -0,0 +1,20 @@ + + +## sort +Collection of challenge list sort + +* [challenge_sort](#module_challenge_sort) + * [.SORTS](#module_challenge_sort.BSORTS) + * [.SORTS_DATA](#module_challenge_sort.SORTS_DATA) + + + +### challenge_sort.SORTS +Sort types +**Kind**: static constant of [challenge_sort](#module_challenge_sort) + + + +### challenge_sort.SORTS_DATA +The data of sort +**Kind**: static constant of [challenge_sort](#module_challenge_sort) diff --git a/docs/tc.md b/docs/tc.md index 8e060798..1345ed26 100644 --- a/docs/tc.md +++ b/docs/tc.md @@ -11,23 +11,34 @@ Collection of small Topcoder-related functions. * [tc](#module_tc) * [.REVIEW_OPPORTUNITY_TYPES](#module_tc.REVIEW_OPPORTUNITY_TYPES) * [.getApiResponsePayload(res)](#module_tc.getApiResponsePayload) ⇒ Promise + * [.processSRM(res)](#module_tc.processSRM) ⇒ Promise ### tc.REVIEW_OPPORTUNITY_TYPES Review Opportunity types -**Kind**: static constant of [tc](#module_tc) +**Kind**: static constant of [tc](#module_tc) ### tc.getApiResponsePayload(res) ⇒ Promise -Gets payload from a standard success response from TC v2 API; or throws +Gets payload from a standard success response from TC API; or throws an error in case of a failure response. -**Kind**: static method of [tc](#module_tc) +**Kind**: static method of [tc](#module_tc) **Returns**: Promise - Resolves to the payload. | Param | Type | | --- | --- | | res | Object | + +### tc.processSRM(res) ⇒ Promise +process srm to populate additional infomation + +**Kind**: static method of [tc](#module_tc) +**Returns**: Promise - Resolves to the payload. + +| Param | Type | +| --- | --- | +| res | Object | diff --git a/docs/url.md b/docs/url.md new file mode 100644 index 00000000..acdfb108 --- /dev/null +++ b/docs/url.md @@ -0,0 +1,27 @@ + + +## url +Collection of url functions. + +* [url](#module_url) + * [.updateQuery](#module_url.updateQuery) + * [.removeTrailingSlash(res)](#module_url.removeTrailingSlash) ⇒ Promise + + +### url.updateQuery +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. +**Kind**: static method of [tc](#module_url) + + + +### url.removeTrailingSlash(res) ⇒ Promise +Cleans/removes trailing slash from url +**Kind**: static method of [url](#module_url) +**Returns**: Promise - Resolves to the payload. + +| Param | Type | +| --- | --- | +| res | String | + diff --git a/src/actions/challenge-listing.js b/src/actions/challenge-listing.js new file mode 100644 index 00000000..d8acf22a --- /dev/null +++ b/src/actions/challenge-listing.js @@ -0,0 +1,450 @@ +/** + * Challenge listing actions. + */ + +import _ from 'lodash'; +import { createActions } from 'redux-actions'; +import { decodeToken } from 'tc-accounts'; +import 'isomorphic-fetch'; +import { processSRM, COMPETITION_TRACKS } from '../utils/tc'; +import { services } from '../services'; +import { errors } from '../utils'; +import * as filterUtil from '../utils/challenge/filter'; +import * as config from '../config'; + +const { fireErrorMessage } = errors; +const { getService } = services.challenge; +const { getReviewOpportunitiesService } = services.reviewOpportunities; +const { PAGE_SIZE, REVIEW_OPPORTUNITY_PAGE_SIZE } = config; + +/** + * Process filter + * Development challenges having Data Science tech tag, still should be + * included into data science track. + * @param filter + * @returns {string} + */ +function processFilter(filter) { + const newFilter = _.clone(filter); + if (_.has(filter, 'track') + && filter.track.includes(COMPETITION_TRACKS.DATA_SCIENCE.toUpperCase()) + && !filter.track.includes(COMPETITION_TRACKS.DEVELOP.toUpperCase()) + ) { + newFilter.track = `${newFilter.track},${COMPETITION_TRACKS.DEVELOP.toUpperCase()}`; + } + return newFilter; +} + +/** + * Private. Loads from the backend all challenges matching some conditions. + * @param {Function} getter Given params object of shape { limit, offset } + * loads from the backend at most "limit" challenges, skipping the first + * "offset" ones. Returns loaded challenges as an array. + * @param {Number} page Optional. Next page of challenges to load. + * @param {Array} prev Optional. Challenges loaded so far. + */ +function getAll(getter, page = 0, prev) { + /* Amount of challenges to fetch in one API call. 50 is the current maximum + * amount of challenges the backend returns, event when the larger limit is + * explicitely required. */ + return getter({ + limit: PAGE_SIZE, + offset: page * PAGE_SIZE, + }).then((res) => { + if (res.challenges.length === 0) { + return prev || res; + } + // parse challenges and meta + let current = {}; + if (prev) { + current.challenges = prev.challenges.concat(res.challenges); + current.meta = res.meta; + } else { + current = res; + } + return getAll(getter, 1 + page, current); + }); +} + +/** + * Gets possible challenge subtracks. + * @return {Promise} + */ +function getChallengeSubtracksDone() { + return getService() + .getChallengeSubtracks() + .then(res => res.sort((a, b) => a.name.localeCompare(b.name))); +} + +/** + * Gets possible challenge tags (technologies). + * @return {Promise} + */ +function getChallengeTagsDone() { + return getService() + .getChallengeTags() + .then(res => res.map(item => item.name) + .sort((a, b) => a.localeCompare(b))); +} + +/** + * Notifies about reloading of all active challenges. The UUID is stored in the + * state, and only challenges fetched by getAllActiveChallengesDone action with + * the same UUID will be accepted into the state. + * @param {String} uuid + * @param {String} page + * @param {Object} frontFilter + * @param {String} sort + * @param {String} bucket + * @return {String} + */ +function getActiveChallengesInit(uuid, page, frontFilter, sort, bucket) { + return { + uuid, page, frontFilter, sort, bucket, + }; +} + +/** 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 getAllActiveChallengesInit(uuid) { + return uuid; +} +function getAllActiveChallengesDone(uuid, tokenV3) { + const filter = { status: 'ACTIVE' }; + const service = getService(tokenV3); + const calls = [ + getAll(params => service.getChallenges(filter, params)), + ]; + let user; + if (tokenV3) { + user = decodeToken(tokenV3).handle; + // Handle any errors on this endpoint so that the non-user specific challenges + // will still be loaded. + calls.push(getAll(params => service.getUserChallenges(user, filter, params) + .catch(() => ({ challenges: [] })))); + } + return Promise.all(calls).then(([ch, uch]) => { + /* uch array contains challenges where the user is participating in +@@ -111,8 +124,8 @@ function getAllActiveChallengesDone(uuid, tokenV3) { + * challenges in an efficient way. */ + if (uch) { + const map = {}; + uch.challenges.forEach((item) => { map[item.id] = item; }); + ch.challenges.forEach((item) => { + if (map[item.id]) { + /* It is fine to reassing, as the array we modifying is created just + * above within the same function. */ + /* eslint-disable no-param-reassign */ + item.users[user] = true; + item.userDetails = map[item.id].userDetails; + /* eslint-enable no-param-reassign */ + } + }); + } + + return { uuid, challenges: ch.challenges }; + }); +} + +/** + * Gets 1 page of active challenges (including marathon matches) from the backend. + * Once this action is completed any active challenges saved to the state before + * will be dropped, and the newly fetched ones will be stored there. + * Loading of all challenges wil start in background. + * @param {String} uuid + * @param {Number} page + * @param {Object} backendFilter Backend filter to use. + * @param {String} tokenV3 Optional. Topcoder auth token v3. Without token only + * public challenges will be fetched. With the token provided, the action will + * also fetch private challenges related to this user. + * @param {Object} frontFilter + * @param {String} sort + * @param {String} bucket + + * @return {Promise} + */ +function getActiveChallengesDone( + uuid, page, backendFilter, tokenV3, frontFilter = {}, sort, bucket, +) { + const filter = processFilter({ + ...backendFilter, + status: 'ACTIVE', + }); + + const service = getService(tokenV3); + const calls = [ + service.getChallenges(filter, { + limit: PAGE_SIZE, + offset: page * PAGE_SIZE, + }), + ]; + let user; + if (tokenV3) { + user = decodeToken(tokenV3).handle; + // Handle any errors on this endpoint so that the non-user specific challenges + // will still be loaded. + calls.push(service.getUserChallenges(user, filter, { + limit: PAGE_SIZE, + offset: page * PAGE_SIZE, + }).catch(() => ({ challenges: [] }))); + } + return Promise.all(calls).then(([ch, uch]) => { + /* uch array contains challenges where the user is participating in + * some role. The same challenge are already listed in res array, but they + * are not attributed to the user there. This block of code marks user + * challenges in an efficient way. */ + if (uch) { + const map = {}; + uch.challenges.forEach((item) => { map[item.id] = item; }); + ch.challenges.forEach((item) => { + if (map[item.id]) { + /* It is fine to reassing, as the array we modifying is created just + * above within the same function. */ + /* eslint-disable no-param-reassign */ + item.users[user] = true; + item.userDetails = map[item.id].userDetails; + /* eslint-enable no-param-reassign */ + } + }); + } + + let { challenges, meta } = ch; + // filter by date range and re-compute meta + // we can safely remove the next two lines when backend support date range + challenges = filterUtil.filterByDate(challenges, frontFilter); + meta = filterUtil.newMeta(meta, challenges, frontFilter); + return { + uuid, + handle: tokenV3 ? user : null, + challenges, + meta, + frontFilter, + sort, + bucket, + tokenV3, + }; + }); +} + +/** + * Init loading of all challenges + * @param {String} uuid + */ +function getRestActiveChallengesInit(uuid) { + return { uuid }; +} + +/** + * Loading all challenges + * @param {String} uuid + * @param {String} tokenV3 + * @param {Object} backendFilter + * @param {Object} frontFilter + * @param {String} sort + * @param {String} bucket + */ +function getRestActiveChallengesDone( + uuid, tokenV3, backendFilter, frontFilter, sort, bucket, +) { + const filter = processFilter({ + ...backendFilter, + status: 'ACTIVE', + }); + + const service = getService(tokenV3); + const calls = [ + getAll(params => service.getChallenges(filter, params), 1), + ]; + let user; + if (tokenV3) { + user = decodeToken(tokenV3).handle; + calls.push(getAll(params => service.getUserChallenges(user, filter, params) + .catch(() => ({ challenges: [] }))), 1); + } + return Promise.all(calls).then(([ch, uch]) => { + /* uch array contains challenges where the user is participating in + * some role. The same challenge are already listed in res array, but they + * are not attributed to the user there. This block of code marks user + * challenges in an efficient way. */ + if (uch) { + const map = {}; + uch.challenges.forEach((item) => { map[item.id] = item; }); + ch.challenges.forEach((item) => { + if (map[item.id]) { + /* It is fine to reassing, as the array we modifying is created just + * above within the same function. */ + /* eslint-disable no-param-reassign */ + item.users[user] = true; + item.userDetails = map[item.id].userDetails; + /* eslint-enable no-param-reassign */ + } + }); + } + + let { challenges } = ch; + // filter by date range and re-compute meta + // we can safely remove the next two lines when backend support date range + challenges = filterUtil.filterByDate(challenges, frontFilter); + const meta = filterUtil.newMeta(undefined, challenges, frontFilter); + + return { + uuid, + handle: tokenV3 ? user : null, + challenges, + frontFilter, + meta, + sort, + bucket, + }; + }); +} + +/** + * Notifies the state that we are about to load the specified page of past + * challenges. + * @param {String} uuid + * @param {Number} page + * @param {Object} frontFilter + * @param {String} sort + * @return {Object} + */ +function getPastChallengesInit(uuid, page, frontFilter, sort) { + return { + uuid, + page, + frontFilter, + sort, + }; +} + +/** + * Gets the specified page of past challenges (including MMs). + * @param {String} uuid + * @param {Number} page Page of challenges to fetch. + * @param {Object} filter Backend filter to use. + * @param {String} tokenV3 Optional. Topcoder auth token v3. + * @param {Object} frontFilter Optional. Original frontend filter. + * @param {String} sort + * @return {Object} + */ +function getPastChallengesDone(uuid, page, filter, tokenV3, frontFilter = {}, sort) { + const service = getService(tokenV3); + const newFilter = processFilter({ + ...filter, + status: 'COMPLETED', + }); + return service.getChallenges(newFilter, { + limit: PAGE_SIZE, + offset: page * PAGE_SIZE, + }).then(({ challenges }) => ({ + uuid, + challenges, + frontFilter, + sort, + })); +} + +/** + * Action to get a list of currently open Review Opportunities using V3 API + * @param {String} uuid Unique identifier for init/donen instance from shortid module + * @param {Number} page Page of review opportunities to fetch. + * @param {String} tokenV3 Optional. + * @param {String} sort Optional. + * @param {Object} frontFilter Optional. + * @return {Object} Action object + */ +function getReviewOpportunitiesDone(uuid, page, tokenV3, sort, frontFilter = {}) { + return getReviewOpportunitiesService(tokenV3) + .getReviewOpportunities(REVIEW_OPPORTUNITY_PAGE_SIZE, page * REVIEW_OPPORTUNITY_PAGE_SIZE) + .then(loaded => ({ + uuid, loaded, sort, frontFilter, + })) + .catch((error) => { + fireErrorMessage('Error Getting Review Opportunities', error.content || error); + return Promise.reject(error); + }); +} + +/** + * Payload creator for the action that inits the loading of SRMs. + * @param {String} uuid + * @return {String} + */ +function getSrmsInit(uuid) { + return uuid; +} + +/** + * Payload creator for the action that loads SRMs. + * @param {String} uuid + * @param {String} handle + * @param {Object} params + * @param {String} tokenV3 + */ +function getSrmsDone(uuid, handle, params, tokenV3) { + const service = getService(tokenV3); + const promises = [service.getSrms(params)]; + if (handle) { + promises.push(service.getUserSrms(handle, params)); + } + return Promise.all(promises).then((data) => { + let srms = data[0]; + const userSrms = data[1]; + const userSrmsMap = {}; + _.forEach(userSrms, (srm) => { + userSrmsMap[srm.id] = srm; + }); + srms = _.map(srms, (srm) => { + if (userSrmsMap[srm.id]) { + return processSRM(srm); + } + return srm; + }); + return { uuid, data: srms }; + }); +} + +export default createActions({ + CHALLENGE_LISTING: { + DROP_CHALLENGES: bucket => ({ bucket }), + + GET_MORE_CHALLENGES: bucket => ({ bucket }), + + GET_ALL_ACTIVE_CHALLENGES_INIT: getAllActiveChallengesInit, + GET_ALL_ACTIVE_CHALLENGES_DONE: getAllActiveChallengesDone, + + GET_ACTIVE_CHALLENGES_INIT: getActiveChallengesInit, + GET_ACTIVE_CHALLENGES_DONE: getActiveChallengesDone, + + GET_REST_ACTIVE_CHALLENGES_INIT: getRestActiveChallengesInit, + GET_REST_ACTIVE_CHALLENGES_DONE: getRestActiveChallengesDone, + + GET_CHALLENGE_SUBTRACKS_INIT: _.noop, + GET_CHALLENGE_SUBTRACKS_DONE: getChallengeSubtracksDone, + + GET_CHALLENGE_TAGS_INIT: _.noop, + GET_CHALLENGE_TAGS_DONE: getChallengeTagsDone, + + GET_PAST_CHALLENGES_INIT: getPastChallengesInit, + GET_PAST_CHALLENGES_DONE: getPastChallengesDone, + + GET_REVIEW_OPPORTUNITIES_INIT: (uuid, page, sort) => ({ uuid, page, sort }), + GET_REVIEW_OPPORTUNITIES_DONE: getReviewOpportunitiesDone, + + GET_SRMS_INIT: getSrmsInit, + GET_SRMS_DONE: getSrmsDone, + + EXPAND_TAG: id => id, + + /* Pass in community ID. */ + SELECT_COMMUNITY: _.identity, + + SET_FILTER: _.identity, + + SET_DATEPICKER_STATUS: status => ({ status }), + + SET_SORT: (bucket, sort) => ({ bucket, sort }), + }, +}); diff --git a/src/actions/index.js b/src/actions/index.js index 8b1a241b..db1d6f66 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -13,6 +13,7 @@ import reviewOpportunityActions from './reviewOpportunity'; import lookupActions from './lookup'; import settingsActions from './settings'; import lookerActions from './looker'; +import challengeListingActions from './challenge-listing'; export const actions = { auth: authActions.auth, @@ -30,6 +31,7 @@ export const actions = { lookup: lookupActions.lookup, settings: settingsActions.settings, looker: lookerActions.looker, + challengeListing: challengeListingActions.challengeListing, }; export default undefined; diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 00000000..2a5ab790 --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,4 @@ +module.exports = { + PAGE_SIZE: 50, + REVIEW_OPPORTUNITY_PAGE_SIZE: 1000, +}; diff --git a/src/index.js b/src/index.js index a108716a..15b0e7d7 100644 --- a/src/index.js +++ b/src/index.js @@ -12,5 +12,5 @@ export { actions } from './actions'; export { services } from './services'; export { - challenge, logger, errors, tc, time, mock, + challenge, logger, errors, tc, time, mock, url, } from './utils'; diff --git a/src/reducers/challenge-listing.js b/src/reducers/challenge-listing.js new file mode 100644 index 00000000..b1d87967 --- /dev/null +++ b/src/reducers/challenge-listing.js @@ -0,0 +1,848 @@ +/** + * Reducer for state.challengeListing. + */ + +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import moment from 'moment'; +import { updateQuery } from '../utils/url'; +import { SORTS_DATA } from '../utils/challenge/sort'; +import actions from '../actions/challenge-listing'; +import { logger, errors, challenge as challengeUtils } from '../utils'; + +const { fireErrorMessage } = errors; +const { filter: Filter } = challengeUtils; +const { BUCKETS, BUCKET_DATA, getBuckets } = challengeUtils.buckets; + +/** + * Process challenge data for bucket + * @param handle user handle + * @param challenges all challenges + * @param loaded fetched challenges of bucket + * @param bucket bucket name + * @param sorts all sorts data + * @param sort sort name + * @param filter filter object + * @param frontFilter filter object + */ +function processBucketData(handle, challenges, loaded, bucket, sorts, sort, filter, frontFilter) { + const buckets = _.isEmpty(handle) ? BUCKET_DATA : getBuckets(handle); + const data = _.has(challenges, bucket) ? challenges[bucket] + .filter(filter) + .concat(loaded) : _.clone(loaded); + + const finalFilters = { + ...frontFilter, + ...buckets[bucket].filter, + }; + + const bucketFilter = bucket !== BUCKETS.REVIEW_OPPORTUNITIES + ? Filter.getFilterFunction(finalFilters) + : Filter.getReviewOpportunitiesFilterFunction(finalFilters); + const filteredData = []; + for (let i = 0; i < data.length; i += 1) { + if (bucketFilter(data[i])) { + filteredData.push(data[i]); + } + } + + if (bucket !== BUCKETS.ALL) { + if (!_.isEmpty(sort)) { + filteredData.sort(SORTS_DATA[sort].func); + return filteredData; + } + + if (_.has(sorts, bucket)) { + filteredData.sort(SORTS_DATA[sorts[bucket]].func); + } else { + filteredData.sort(SORTS_DATA[BUCKET_DATA[bucket].sorts[0]].func); + } + } + + return filteredData; +} + +/** + * Check the challenges of bucket have been loaded all + * @param challenges all challenges + * @param bucket bucket name + * @param loaded loaded challenges this time + * @param data processed data + * @returns {boolean} + */ +function checkAllLoaded(challenges, bucket, loaded, data) { + let isAll = false; + if (loaded.length === 0) { + isAll = true; + } else if (!_.isEmpty(_.get(challenges, bucket)) + && challenges[bucket].length === data.length) { + isAll = true; + } + + return isAll; +} + +/** 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, sort, bucket, tokenV3, handle, frontFilter, + meta, + } = payload; + + /* 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)); + + let filter; + let newChallenges = {}; + const otherState = {}; + switch (bucket) { + case BUCKETS.ALL: { + if (uuid !== state.loadingActiveChallengesUUID) return state; + /* Fetching 0 page of active challenges also drops any active challenges + * loaded to the state before. */ + filter = state.lastRequestedPageOfActiveChallenges + ? item => !ids.has(item.id) + : item => !ids.has(item.id) && item.status !== 'ACTIVE'; + + // my + const my = !_.isEmpty(tokenV3) ? processBucketData( + handle, state.challenges, loaded, BUCKETS.MY, state.sorts, sort, filter, + ) : []; + // open for registration + const open = processBucketData( + handle, state.challenges, loaded, BUCKETS.OPEN_FOR_REGISTRATION, state.sorts, sort, filter, + ); + // ongoing + const ongoing = processBucketData( + handle, state.challenges, loaded, BUCKETS.ONGOING, state.sorts, sort, filter, + ); + newChallenges = _.clone(state.challenges); + newChallenges[BUCKETS.MY] = my; + newChallenges[BUCKETS.OPEN_FOR_REGISTRATION] = open; + newChallenges[BUCKETS.ONGOING] = ongoing; + otherState.loadingActiveChallengesUUID = ''; + otherState.meta = _.clone(meta); + } + break; + case BUCKETS.MY: { + if (uuid !== state.loadingMyChallengesUUID) return state; + /* Fetching 0 page of active challenges also drops any active challenges + * loaded to the state before. */ + filter = state.lastRequestedPageOfMyChallenges + ? item => !ids.has(item.id) + : item => !ids.has(item.id) && item.status !== 'ACTIVE'; + + const data = processBucketData( + handle, state.challenges, loaded, bucket, state.sorts, sort, filter, + ); + newChallenges = _.cloneDeep(state.challenges); + newChallenges[bucket] = data; + otherState.loadingMyChallengesUUID = ''; + otherState.allMyChallengesLoaded = checkAllLoaded(state.challenges, bucket, loaded, data); + otherState.gettingMoreMyChallenges = !otherState.allMyChallengesLoaded; + otherState.meta = _.clone(meta); + /* TODO Due to the meta of backend response is currently not correct, +/* so should update counts after fetch all challenges of bucket */ + if (_.get(meta, 'myChallengesCount') !== data.length && otherState.allMyChallengesLoaded) { + otherState.meta.myChallengesCount = data.length; + otherState.meta.allChallengesCount = meta.allChallengesCount + + data.length - meta.myChallengesCount; + } + } + break; + case BUCKETS.OPEN_FOR_REGISTRATION: { + if (uuid !== state.loadingOpenChallengesUUID) return state; + /* Fetching 0 page of active challenges also drops any active challenges + * loaded to the state before. */ + filter = state.lastRequestedPageOfOpenChallenges + ? item => !ids.has(item.id) + : item => !ids.has(item.id) && item.status !== 'ACTIVE'; + + const data = processBucketData( + handle, state.challenges, loaded, bucket, state.sorts, sort, filter, + ); + + newChallenges = _.cloneDeep(state.challenges); + newChallenges[bucket] = data; + otherState.loadingOpenChallengesUUID = ''; + otherState.allOpenChallengesLoaded = checkAllLoaded(state.challenges, bucket, loaded, data); + otherState.gettingMoreOpenChallenges = !otherState.allOpenChallengesLoaded; + otherState.meta = _.clone(meta); + /* TODO Due to the meta of backend response is currently not correct, + /* so should update counts after fetch all challenges of bucket */ + if (_.get(meta, 'openChallengesCount') !== data.length && otherState.allOpenChallengesLoaded) { + otherState.meta.openChallengesCount = data.length; + otherState.meta.allChallengesCount = meta.allChallengesCount + + data.length - meta.openChallengesCount; + } + } + break; + case BUCKETS.ONGOING: { + if (uuid !== state.loadingOnGoingChallengesUUID) return state; + /* Fetching 0 page of active challenges also drops any active challenges + * loaded to the state before. */ + filter = state.lastRequestedPageOfOnGoingChallenges + ? item => !ids.has(item.id) + : item => !ids.has(item.id) && item.status !== 'ACTIVE'; + + const data = processBucketData( + handle, state.challenges, loaded, bucket, state.sorts, sort, filter, + ); + newChallenges = _.cloneDeep(state.challenges); + newChallenges[bucket] = data; + otherState.loadingOnGoingChallengesUUID = ''; + otherState.allOnGoingChallengesLoaded = checkAllLoaded(state.challenges, + bucket, loaded, data); + otherState.gettingMoreOnGoingChallenges = !otherState.allOnGoingChallengesLoaded; + /* TODO Due to the meta of backend response is currently not correct, + /* so should update counts after fetch all challenges of bucket */ + otherState.meta = _.clone(meta); + if (_.get(meta, 'ongoingChallengesCount') !== data.length && otherState.allOnGoingChallengesLoaded) { + otherState.meta.ongoingChallengesCount = data.length; + otherState.meta.allChallengesCount = meta.allChallengesCount + + data.length - meta.ongoingChallengesCount; + } + } + break; + default: + break; + } + + // all challenges used for other components like sub communities + newChallenges[BUCKETS.ALL] = processBucketData( + handle, state.challenges, loaded, BUCKETS.ALL, null, null, filter, frontFilter, + ); + + return { + ...state, + ...otherState, + challenges: newChallenges, + lastUpdateOfActiveChallenges: Date.now(), + }; +} + +/** + * Called when loading of 1st page of active challenges is started + * @param {*} state + * @param {*} param1 + */ +function onGetActiveChallengesInit(state, { payload }) { + const { page, bucket, uuid } = payload; + const otherState = {}; + switch (bucket) { + case BUCKETS.ALL: + otherState.loadingActiveChallengesUUID = uuid; + otherState.lastRequestedPageOfActiveChallenges = page; + break; + case BUCKETS.MY: + otherState.loadingMyChallengesUUID = uuid; + otherState.lastRequestedPageOfMyChallenges = page; + break; + case BUCKETS.OPEN_FOR_REGISTRATION: + otherState.loadingOpenChallengesUUID = uuid; + otherState.lastRequestedPageOfOpenChallenges = page; + break; + case BUCKETS.ONGOING: + otherState.loadingOnGoingChallengesUUID = uuid; + otherState.lastRequestedPageOfOnGoingChallenges = page; + break; + default: + break; + } + + return { + ...state, + ...otherState, + }; +} +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, meta: newMeta, sort, bucket, handle, frontFilter, + } = 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 otherState = {}; + let newChallenges = {}; + switch (bucket) { + case BUCKETS.MY: + case BUCKETS.OPEN_FOR_REGISTRATION: + case BUCKETS.ONGOING: { + const data = processBucketData( + handle, state.challenges, loaded, bucket, state.sorts, sort, filter, frontFilter, + ); + newChallenges = _.cloneDeep(state.challenges); + newChallenges[bucket] = data; + switch (bucket) { + case BUCKETS.MY: + otherState.allMyChallengesLoaded = true; + otherState.gettingMoreMyChallenges = false; + break; + case BUCKETS.OPEN_FOR_REGISTRATION: + otherState.allOpenChallengesLoaded = true; + otherState.gettingMoreOpenChallenges = false; + break; + case BUCKETS.ONGOING: + otherState.allOnGoingChallengesLoaded = true; + otherState.gettingMoreOnGoingChallenges = false; + break; + default: + break; + } + } + break; + default: + break; + } + + const meta = newMeta || state.meta; + + return { + ...state, + challenges: newChallenges, + ...otherState, + meta, + 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, sort, + } = 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 pasts = processBucketData( + null, state.challenges, loaded, BUCKETS.PAST, state.sorts, sort, filter, frontFilter, + ); + + let keepPastPlaceholders = false; + if (loaded.length) { + const ff = Filter.getFilterFunction(frontFilter); + keepPastPlaceholders = pasts.filter(ff).length + - (_.has(state.challenges, BUCKETS.PAST) + ? state.challenges[BUCKETS.PAST].filter(ff).length : 0) < 10; + } + + const newChallenges = _.cloneDeep(state.challenges); + newChallenges[BUCKETS.PAST] = pasts; + + return { + ...state, + allPastChallengesLoaded: loaded.length === 0, + challenges: newChallenges, + 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 carefully (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, + sort, + frontFilter, + } = payload; + + if (uuid !== state.loadingReviewOpportunitiesUUID) return state; + + const ids = new Set(); + loaded.forEach(item => ids.add(item.id)); + + const filter = item => !ids.has(item.id); + + const reviewOpportunities = processBucketData( + null, state, loaded, BUCKETS.REVIEW_OPPORTUNITIES, + state.sorts, sort, filter, frontFilter, + ); + + 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, { payload }) => { + const { bucket } = payload; + const otherState = {}; + switch (bucket) { + case BUCKETS.REVIEW_OPPORTUNITIES: + otherState.lastRequestedPageOfReviewOpportunities = -1; + otherState.reviewOpportunities = []; + otherState.allReviewOpportunitiesLoaded = false; + break; + case BUCKETS.PAST: + otherState.challenges = _.cloneDeep(state.challenges); + otherState.lastRequestedPageOfPastChallenges = -1; + otherState.challenges.past = []; + otherState.allPastChallengesLoaded = false; + break; + default: + otherState.challenges = {}; + otherState.allMyChallengesLoaded = false; + otherState.allOnGoingChallengesLoaded = false; + otherState.allOpenChallengesLoaded = false; + otherState.allActiveChallengesLoaded = false; + otherState.allPastChallengesLoaded = false; + otherState.allReviewOpportunitiesLoaded = false; + otherState.lastRequestedPageOfActiveChallenges = -1; + otherState.lastRequestedPageOfMyChallenges = -1; + otherState.lastRequestedPageOfOpenChallenges = -1; + otherState.lastRequestedPageOfOnGoingChallenges = -1; + otherState.lastRequestedPageOfPastChallenges = -1; + otherState.lastRequestedPageOfReviewOpportunities = -1; + otherState.lastUpdateOfActiveChallenges = -1; + otherState.loadingActiveChallengesUUID = ''; + otherState.loadingMyChallengesUUID = ''; + otherState.loadingOpenChallengesUUID = ''; + otherState.loadingOnGoingChallengesUUID = ''; + otherState.loadingRestActiveChallengesUUID = ''; + otherState.loadingPastChallengesUUID = ''; + otherState.loadingReviewOpportunitiesUUID = ''; + otherState.reviewOpportunities = []; + otherState.meta = { + allChallengesCount: 0, + myChallengesCount: 0, + ongoingChallengesCount: 0, + openChallengesCount: 0, + totalCount: 0, + }; + break; + } + + return ({ + ...state, + ...otherState, + }); + }, + + [a.getMoreChallenges]: (state, { payload }) => { + const { bucket } = payload; + const otherState = {}; + switch (bucket) { + case BUCKETS.MY: + otherState.gettingMoreMyChallenges = true; + break; + case BUCKETS.ONGOING: + otherState.gettingMoreOnGoingChallenges = true; + break; + case BUCKETS.OPEN_FOR_REGISTRATION: + otherState.gettingMoreOpenChallenges = true; + break; + default: + break; + } + return ({ + ...state, + ...otherState, + }); + }, + + [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 }) => { + const otherState = {}; + switch (payload.bucket) { + case BUCKETS.PAST: + otherState.lastRequestedPageOfPastChallenges = -1; + break; + case BUCKETS.MY: + case BUCKETS.OPEN_FOR_REGISTRATION: + case BUCKETS.ONGOING: + otherState.lastRequestedPageOfActiveChallenges = -1; + break; + case BUCKETS.REVIEW_OPPORTUNITIES: + otherState.lastRequestedPageOfReviewOpportunities = -1; + break; + default: + break; + } + return ({ + ...state, + ...otherState, + sorts: { + ...state.sorts, + [payload.bucket]: payload.sort, + }, + }); + }, + + [a.setDatePickerStatus]: (state, { payload }) => { + const { status } = payload; + return ({ + ...state, + datepickerOpen: status, + }); + }, + }, _.defaults(_.clone(initialState) || {}, { + allMyChallengesLoaded: false, + allOnGoingChallengesLoaded: false, + allOpenChallengesLoaded: false, + allActiveChallengesLoaded: false, + allPastChallengesLoaded: false, + allReviewOpportunitiesLoaded: false, + + challenges: {}, + challengeSubtracks: [], + challengeSubtracksMap: {}, + challengeTags: [], + + expandedTags: [], + + gettingMoreChallenges: false, + gettingMoreMyChallenges: false, + gettingMoreOnGoingChallenges: false, + gettingMoreOpenChallenges: false, + + filter: {}, + + keepPastPlaceholders: false, + + lastRequestedPageOfActiveChallenges: -1, + lastRequestedPageOfMyChallenges: -1, + lastRequestedPageOfOnGoingChallenges: -1, + lastRequestedPageOfOpenChallenges: -1, + lastRequestedPageOfPastChallenges: -1, + lastRequestedPageOfReviewOpportunities: -1, + lastUpdateOfActiveChallenges: 0, + + loadingActiveChallengesUUID: '', + loadingMyChallengesUUID: '', + loadingOnGoingChallengesUUID: '', + loadingOpenChallengesUUID: '', + 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, + }, + + datepickerOpen: false, + })); +} + +/** + * 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. + * @return {Promise} Resolves to the new reducer. + */ +export function factory() { + return Promise.resolve(create()); +} + +/* Default reducer with empty initial state. */ +export default create(); diff --git a/src/reducers/index.js b/src/reducers/index.js index 15fd144c..aee34c61 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -22,6 +22,7 @@ import settings, { factory as settingsFactory } from './settings'; import looker, { factory as lookerFactory } from './looker'; +import challengeListing, { factory as challengeListingFactory } from './challenge-listing'; export function factory(options) { @@ -41,6 +42,7 @@ export function factory(options) { mySubmissionsManagement: mySubmissionsManagementFactory(options), settings: settingsFactory(options), looker: lookerFactory(options), + challengeListing: challengeListingFactory(options), }); } @@ -60,4 +62,5 @@ export default ({ mySubmissionsManagement, settings, looker, + challengeListing, }); diff --git a/src/services/challenges.js b/src/services/challenges.js index 604a2879..04244726 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -244,10 +244,10 @@ class ChallengesService { params = {}, ) => { const query = { - filter: qs.stringify(filters, { encode: false }), + filter: qs.stringify(filters, { encode: false }).replace('&', '%26'), ...params, }; - const url = `${endpoint}?${qs.stringify(query)}`; + const url = `${endpoint}?${qs.stringify(query, { encode: false })}`; const res = await this.private.api.get(url).then(checkError); return { challenges: res.content || [], diff --git a/src/utils/challenge/buckets.js b/src/utils/challenge/buckets.js new file mode 100644 index 00000000..d9e9221c --- /dev/null +++ b/src/utils/challenge/buckets.js @@ -0,0 +1,153 @@ +/** + * Standard challenge buckets + */ + +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', +}; + +export const BUCKET_DATA = { + [BUCKETS.ALL]: { + filter: { + started: true, + status: ['ACTIVE'], + }, + hideCount: false, + name: 'All Challenges', + sorts: [], + }, + [BUCKETS.MY]: { + filter: { + started: true, + status: ['ACTIVE'], + }, + 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/utils/challenge/filter.js b/src/utils/challenge/filter.js index 28e00654..bdd343af 100644 --- a/src/utils/challenge/filter.js +++ b/src/utils/challenge/filter.js @@ -177,6 +177,39 @@ function filterByUsers(challenge, state) { return state.users.find(user => challenge.users[user]); } +/** + * [filterByDate filter challenges by date reange] + * @param {[type]} challenges input challenges + * @param {[type]} filter filter including startDate and endDate + * @return {[type]} filtered challenges array + */ +export function filterByDate(challenges, filter) { + let cs = challenges.filter(c => filterByStartDate(c, filter)); + cs = cs.filter(c => filterByEndDate(c, filter)); + return cs; +} + +/** + * [newMeta compute new meta via challenges and filter] + * @param {[type]} meta old meta + * @param {[type]} challenges input challenges + * @param {[type]} filter filter including startDate and end endDate + * @return {[type]} new meta + */ +export function newMeta(meta, challenges, filter) { + if (!filter.startDate && !filter.endDate) { + return meta; + } + const m = { + }; + m.allChallengesCount = challenges.length; + m.openChallengesCount = challenges.filter(c => c.registrationOpen === 'Yes').length; + m.ongoingChallengesCount = m.allChallengesCount - m.openChallengesCount; + m.myChallengesCount = challenges.filter(c => c.user && !_.isEmpty(c.user)).length; + m.totalCount = challenges.length; + return m; +} + /** * Returns clone of the state with the specified competition track added. * @param {Object} state diff --git a/src/utils/challenge/sort.js b/src/utils/challenge/sort.js new file mode 100644 index 00000000..4091b0c0 --- /dev/null +++ b/src/utils/challenge/sort.js @@ -0,0 +1,84 @@ +/** + * 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 const SORTS_DATA = { + [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/utils/index.js b/src/utils/index.js index b63d63b8..e7e4ff52 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -7,9 +7,14 @@ import * as time from './time'; import * as mock from './mock'; import * as errors from './errors'; import * as filter from './challenge/filter'; +import * as buckets from './challenge/buckets'; +import * as sort from './challenge/sort'; +import * as url from './url'; const challenge = { filter, + buckets, + sort, }; export { @@ -19,4 +24,5 @@ export { time, mock, errors, + url, }; diff --git a/src/utils/tc.js b/src/utils/tc.js index 77fce85a..08309d16 100644 --- a/src/utils/tc.js +++ b/src/utils/tc.js @@ -4,6 +4,9 @@ * @todo More TC-related utils should be moved here from Community-app. */ +import _ from 'lodash'; +import moment from 'moment'; + /** * Codes of the Topcoder communities. */ @@ -60,3 +63,47 @@ export async function getLookerApiResponsePayload(res) { status: x.status, }; } + +/** + * process srm to populate additional infomation + * adopt from topcoder-app repo + * @param {Object} s srm to process + * @return {Object} processed srm + */ +export function processSRM(s) { + const srm = _.cloneDeep(s); + srm.userStatus = 'registered'; + if (Array.isArray(srm.rounds) && srm.rounds.length) { + if (srm.rounds[0].userSRMDetails && srm.rounds[0].userSRMDetails.rated) { + srm.result = srm.rounds[0].userSRMDetails; + } + if (srm.rounds[0].codingStartAt) { + srm.codingStartAt = srm.rounds[0].codingStartAt; + } + if (srm.rounds[0].codingEndAt) { + srm.codingEndAt = srm.rounds[0].codingEndAt; + } + if (srm.rounds[0].registrationStartAt) { + srm.registrationStartAt = srm.rounds[0].registrationStartAt; + } + if (srm.rounds[0].registrationEndAt) { + srm.registrationEndAt = srm.rounds[0].registrationEndAt; + } + } + + // determines if the current phase is registration + let start = moment(srm.registrationStartAt).unix(); + let end = moment(srm.registrationEndAt).unix(); + let now = moment().unix(); + if (start <= now && end >= now) { + srm.currentPhase = 'REGISTRATION'; + } + // determines if the current phase is coding + start = moment(srm.codingStartAt).unix(); + end = moment(srm.codingEndAt).unix(); + now = moment().unix(); + if (start <= now && end >= now) { + srm.currentPhase = 'CODING'; + } + return srm; +} diff --git a/src/utils/url.js b/src/utils/url.js new file mode 100644 index 00000000..73154aa7 --- /dev/null +++ b/src/utils/url.js @@ -0,0 +1,49 @@ +/** + * 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;