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 related to Topcoder challenge-listing APIs.
+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:
Reducer for actions.challenge-listing actions.
+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
Collection of challenge sort.
+Collection of challenge buckets.
+Collection of url function.
+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;