diff --git a/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/__snapshots__/index.jsx.snap
deleted file mode 100644
index 59f37d833..000000000
--- a/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/__snapshots__/index.jsx.snap
+++ /dev/null
@@ -1,62 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Matches shallow shapshot shapshot 1 1`] = `
-
-
-
-
- SUBMISSION
-
-
- 1
-
-
-
-
- FINAL SCORE
-
-
- N/A
-
-
-
-
- PROVISIONAL SCORE
-
-
- 80
-
-
-
-
- TIME
-
-
- 06 Nov 2017
-
- 15:49:35
-
-
-
-
-`;
diff --git a/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx b/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx
deleted file mode 100755
index ed577ffac..000000000
--- a/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react';
-// import ReactDOM from 'react-dom';
-import Renderer from 'react-test-renderer/shallow';
-// import TU from 'react-dom/test-utils';
-import SubmissionHistoryRow from 'components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow';
-
-const mockData = {
- isMM: true,
- submission: 1,
- finalScore: 80,
- provisionalScore: 80,
- submissionTime: '2017-11-06T15:49:35.000Z',
- isReviewPhaseComplete: false,
- status: 'completed',
- numWinners: 1,
- challengeStatus: 'Completed',
- auth: {
- tokenV3: 'tokenV3',
- },
- submissionId: '1',
- isLoggedIn: true,
-};
-
-describe('Matches shallow shapshot', () => {
- test('shapshot 1', () => {
- const renderer = new Renderer();
-
- renderer.render((
-
- ));
- expect(renderer.getRenderOutput()).toMatchSnapshot();
- });
-});
-
-/*
-class Wrapper extends React.Component {
- componentDidMount() {}
-
- render() {
- return ;
- }
-}
-describe('render properly', () => {
- test('click', () => {
- const instance = TU.renderIntoDocument(());
- const matches = TU.scryRenderedDOMComponentsWithTag(instance, 'button');
- expect(matches).toHaveLength(1);
- TU.Simulate.click(matches[0]);
- });
-});
-*/
diff --git a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap
index 588224153..e1664e107 100644
--- a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap
+++ b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap
@@ -38,12 +38,14 @@ exports[`Matches shallow shapshot 1 shapshot 1 1`] = `
auth={Object {}}
challenges={Array []}
communityName={null}
+ copilotOpportunities={Array []}
expandTag={null}
expandedTags={Array []}
expanding={false}
filterState={Object {}}
loadMoreActive={null}
loadMoreAll={null}
+ loadMoreCopilotOpportunities={null}
loadMoreMy={null}
loadMoreMyPast={null}
loadMoreOnGoing={null}
@@ -103,12 +105,14 @@ exports[`Matches shallow shapshot 2 shapshot 2 1`] = `
auth={Object {}}
challenges={Array []}
communityName={null}
+ copilotOpportunities={Array []}
expandTag={null}
expandedTags={Array []}
expanding={false}
filterState={Object {}}
loadMoreActive={null}
loadMoreAll={null}
+ loadMoreCopilotOpportunities={null}
loadMoreMy={null}
loadMoreMyPast={null}
loadMoreOnGoing={null}
diff --git a/config/backup-default.js b/config/backup-default.js
index 2ea3ca1ee..57d4eb617 100644
--- a/config/backup-default.js
+++ b/config/backup-default.js
@@ -102,6 +102,7 @@ module.exports = {
/* This is the same value as above, but it is used by topcoder-react-lib,
* as a more verbose name for the param. */
COMMUNITY_APP: 'https://community-app.topcoder-dev.com',
+ COPILOTS_URL: 'https://copilots.topcoder-dev.com',
CHALLENGES_URL: 'https://www.topcoder-dev.com/challenges',
TCO_OPEN_URL: 'https://www.topcoder-dev.com/community/member-programs/topcoder-open',
ARENA: 'https://arena.topcoder-dev.com',
diff --git a/config/default.js b/config/default.js
index 8474dd287..8f8f4d11a 100644
--- a/config/default.js
+++ b/config/default.js
@@ -104,6 +104,7 @@ module.exports = {
* as a more verbose name for the param. */
COMMUNITY_APP: 'https://community-app.topcoder-dev.com',
CHALLENGES_URL: 'https://www.topcoder-dev.com/challenges',
+ COPILOTS_URL: 'https://copilots.topcoder-dev.com',
TCO_OPEN_URL: 'https://www.topcoder-dev.com/community/member-programs/topcoder-open',
ARENA: 'https://arena.topcoder-dev.com',
AUTH: 'https://accounts-auth0.topcoder-dev.com',
diff --git a/config/production.js b/config/production.js
index a0f0d88b8..afebd77d6 100644
--- a/config/production.js
+++ b/config/production.js
@@ -26,6 +26,7 @@ module.exports = {
* as a more verbose name for the param. */
COMMUNITY_APP: 'https://community-app.topcoder.com',
CHALLENGES_URL: 'https://www.topcoder.com/challenges',
+ COPILOTS_URL: 'https://copilots.topcoder.com',
TCO_OPEN_URL: 'https://www.topcoder.com/community/member-programs/topcoder-open',
AUTH: 'https://accounts-auth0.topcoder.com',
diff --git a/src/shared/actions/challenge-listing/index.js b/src/shared/actions/challenge-listing/index.js
index f7d109ca8..7d7784007 100644
--- a/src/shared/actions/challenge-listing/index.js
+++ b/src/shared/actions/challenge-listing/index.js
@@ -10,6 +10,7 @@ import { processSRM } from 'utils/tc';
import { errors, services } from 'topcoder-react-lib';
import { BUCKETS } from 'utils/challenge-listing/buckets';
import SORT from 'utils/challenge-listing/sort';
+import getCopilotOpportunities from '../../services/copilotOpportunities';
const { fireErrorMessage } = errors;
const { getService } = services.challenge;
@@ -25,6 +26,8 @@ const PAGE_SIZE = 10;
*/
const REVIEW_OPPORTUNITY_PAGE_SIZE = 1000;
+const COPILOT_OPPORTUNITY_PAGE_SIZE = 20;
+
/**
* Private. Loads from the backend all challenges matching some conditions.
* @param {Function} getter Given params object of shape { limit, offset }
@@ -496,6 +499,21 @@ function getReviewOpportunitiesDone(uuid, page, tokenV3) {
});
}
+/**
+ * Action to get a list of currently open Copilot Opportunities using V5 API
+ * @param {String} uuid Unique identifier for init/done instance from shortid module
+ * @param {Number} page Page of copilot opportunities to fetch (1-based)
+ * @return {Promise<{uuid: string, loaded: object}>} Action result
+ */
+function getCopilotOpportunitiesDone(uuid, page) {
+ return getCopilotOpportunities(page, COPILOT_OPPORTUNITY_PAGE_SIZE)
+ .then(loaded => ({ uuid, loaded }))
+ .catch((error) => {
+ fireErrorMessage('Error Getting Copilot Opportunities', error.content || error);
+ return Promise.reject(error);
+ });
+}
+
/**
* Payload creator for the action that inits the loading of SRMs.
* @param {String} uuid
@@ -610,6 +628,9 @@ export default createActions({
GET_REVIEW_OPPORTUNITIES_INIT: (uuid, page) => ({ uuid, page }),
GET_REVIEW_OPPORTUNITIES_DONE: getReviewOpportunitiesDone,
+ GET_COPILOT_OPPORTUNITIES_INIT: (uuid, page) => ({ uuid, page }),
+ GET_COPILOT_OPPORTUNITIES_DONE: getCopilotOpportunitiesDone,
+
GET_SRMS_INIT: getSrmsInit,
GET_SRMS_DONE: getSrmsDone,
diff --git a/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx
new file mode 100644
index 000000000..279ceede0
--- /dev/null
+++ b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx
@@ -0,0 +1,76 @@
+/**
+ * Component for rendering a Copilot Opportunity Card which will serve as a row in the list.
+ */
+import _ from 'lodash';
+import { config } from 'topcoder-react-utils';
+import moment from 'moment';
+import React, { useMemo } from 'react';
+import PT from 'prop-types';
+
+import Tags from '../Tags';
+
+import './style.scss';
+
+const PROJECT_TYPE_LABELS = {
+ dev: 'Development',
+ ai: 'AI (Artificial Intelligence)',
+ design: 'Design',
+ datascience: 'Data Science',
+ qa: 'Quality Assurance',
+};
+
+function CopilotOpportunityCard({
+ opportunity,
+}) {
+ const skills = useMemo(() => _.uniq((opportunity.skills || []).map(skill => skill.name)), [
+ opportunity.skills,
+ ]);
+ const start = moment(opportunity.startDate);
+
+ return (
+
+
+
+
+
+ {PROJECT_TYPE_LABELS[opportunity.type]}
+
+
+ {opportunity.status}
+
+
+ {opportunity.numHoursPerWeek} hours/week
+
+
+
+ );
+}
+
+CopilotOpportunityCard.propTypes = {
+ opportunity: PT.shape().isRequired,
+};
+
+export default CopilotOpportunityCard;
diff --git a/src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss b/src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss
new file mode 100644
index 000000000..162f32a83
--- /dev/null
+++ b/src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss
@@ -0,0 +1,236 @@
+@import '~styles/mixins';
+
+$challenge-space-10: $base-unit * 2;
+$challenge-space-15: $base-unit * 3;
+$challenge-space-20: $base-unit * 4;
+$challenge-space-30: $base-unit * 6;
+$challenge-space-40: $base-unit * 8;
+$challenge-space-45: $base-unit * 9;
+$challenge-space-50: $base-unit * 10;
+$challenge-space-90: $base-unit * 18;
+$status-space-10: $base-unit * 2;
+$status-space-15: $base-unit * 3;
+$status-space-20: $base-unit * 4;
+$status-space-25: $base-unit * 5;
+$status-space-30: $base-unit * 6;
+$status-space-40: $base-unit * 8;
+$status-space-50: $base-unit * 10;
+$status-radius-1: $corner-radius / 2;
+$status-radius-4: $corner-radius * 2;
+
+.copilotOpportunityCard {
+ @include roboto-medium;
+
+ display: flex;
+ justify-content: flex-start;
+ position: relative;
+ background: $tc-white;
+ padding: $challenge-space-20 0;
+ border-top: 1px solid $tc-gray-10;
+ color: $tc-black;
+ font-size: 15px;
+ margin-left: 24px;
+
+ &:last-child {
+ border-bottom: 1px solid $tc-gray-10;
+ }
+
+ @include xs-to-md {
+ flex-wrap: wrap;
+ padding: $base-unit * 3 0;
+ margin-left: 0;
+ flex-direction: column;
+ }
+
+ @include xs-to-sm {
+ position: relative;
+ }
+
+ a,
+ a:visited {
+ color: $tc-black;
+ }
+
+ a:hover {
+ color: $tc-dark-blue-110;
+ }
+
+ .left-panel {
+ display: flex;
+ justify-content: flex-start;
+ width: 45.5%;
+
+ @include xs-to-md {
+ width: 100%;
+ padding: 0 16px;
+ }
+ }
+
+ .right-panel {
+ display: flex;
+ justify-content: space-between;
+ width: 50%;
+
+ @include xs-to-md {
+ width: 100%;
+ display: flex;
+ }
+
+ @include xs-to-sm {
+ display: flex;
+ }
+ }
+
+ // Challenge title, end date & technologies
+ .challenge-details {
+ display: inline-block;
+ vertical-align: baseline;
+ width: 82%;
+ margin-right: $challenge-space-30;
+
+ @include md {
+ margin-right: 180px;
+ }
+
+ @include xs-to-sm {
+ margin-right: 0;
+ }
+
+ a {
+ line-height: $challenge-space-20;
+
+ @include xs-to-sm {
+ display: inline-block;
+ }
+ }
+ }
+
+ .details-footer {
+ width: 100%;
+ margin-top: 16px;
+ display: flex;
+
+ .date {
+ font-size: 12px;
+ color: $tc-gray-60;
+ margin-right: $challenge-space-10;
+ line-height: ($challenge-space-10) + 2;
+ font-weight: normal;
+ margin-top: 2px;
+ }
+ }
+
+ // Review payment
+ .status {
+ @include roboto-medium;
+
+ display: inline-block;
+ font-size: 13px;
+ font-weight: 500;
+ color: green;
+ line-height: $challenge-space-20;
+ margin-right: $challenge-space-20;
+ min-width: $challenge-space-50 + 2;
+ width: 30%;
+
+ &.completed {
+ color: $tc-orange;
+ }
+
+ @include xs-to-md {
+ position: absolute;
+ right: 0;
+ top: 20px;
+ margin-right: $challenge-space-20;
+ margin-bottom: $challenge-space-20;
+ }
+
+ @include xs-to-sm {
+ position: relative;
+ display: block;
+ margin-top: $challenge-space-30;
+ margin-bottom: $challenge-space-45;
+ margin-left: $challenge-space-15;
+ top: 0;
+ }
+
+ @include md {
+ right: 108px;
+ }
+
+ // $ Symbol
+ span {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 16px;
+ text-transform: capitalize;
+ }
+ }
+
+ .type {
+ @include roboto-medium;
+
+ width: 40%;
+
+ @include xs-to-md {
+ position: absolute;
+ right: 0;
+ top: 20px;
+ margin-right: $challenge-space-20;
+ margin-bottom: $challenge-space-20;
+ }
+
+ @include xs-to-sm {
+ position: relative;
+ display: block;
+ margin-top: $challenge-space-30;
+ margin-bottom: $challenge-space-45;
+ margin-left: $challenge-space-15;
+ top: 0;
+ }
+
+ @include md {
+ right: 108px;
+ }
+
+ span {
+ color: $tc-black;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 16px;
+ text-transform: capitalize;
+ }
+ }
+
+ .numHours {
+ width: 30%;
+
+ @include xs-to-md {
+ position: absolute;
+ right: 0;
+ top: 20px;
+ margin-right: $challenge-space-20;
+ margin-bottom: $challenge-space-20;
+ }
+
+ @include xs-to-sm {
+ position: relative;
+ display: block;
+ margin-top: $challenge-space-30;
+ margin-bottom: $challenge-space-45;
+ margin-left: $challenge-space-15;
+ top: 0;
+ }
+
+ @include md {
+ right: 108px;
+ }
+
+ span {
+ color: $tc-black;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 16px;
+ }
+ }
+}
diff --git a/src/shared/components/challenge-listing/CopilotOpportunityHeader/index.jsx b/src/shared/components/challenge-listing/CopilotOpportunityHeader/index.jsx
new file mode 100644
index 000000000..c45804169
--- /dev/null
+++ b/src/shared/components/challenge-listing/CopilotOpportunityHeader/index.jsx
@@ -0,0 +1,33 @@
+/**
+ * Component for rendering a Copilot Opportunity and associated Challenge
+ * information. Will be contained within a Bucket.
+ */
+import React from 'react';
+import './style.scss';
+
+function CopilotOpportunityHeader() {
+ return (
+
+
+
+
+ Opportunity Name
+
+
+
+
+
+ Type
+
+
+ Status
+
+
+ Commitment
+
+
+
+ );
+}
+
+export default CopilotOpportunityHeader;
diff --git a/src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss b/src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss
new file mode 100644
index 000000000..77716dc91
--- /dev/null
+++ b/src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss
@@ -0,0 +1,90 @@
+@import '~styles/mixins';
+
+$challenge-space-20: $base-unit * 4;
+$challenge-space-30: $base-unit * 6;
+
+.copilotOpportunityHeader {
+ @include roboto-medium;
+
+ display: flex;
+ justify-content: flex-start;
+ position: relative;
+ background: $tc-white;
+ padding: $challenge-space-20 0;
+ color: $tc-gray-60;
+ font-size: 12px;
+ margin-left: 24px;
+ text-transform: uppercase;
+
+ @include xs-to-md {
+ flex-wrap: wrap;
+ padding: $base-unit * 3 0;
+ margin-left: 0;
+ flex-direction: column;
+ }
+
+ @include xs-to-sm {
+ position: relative;
+ }
+
+ .left-panel {
+ display: flex;
+ justify-content: flex-start;
+ width: 45.5%;
+
+ @include xs-to-md {
+ width: 100%;
+ padding: 0 16px;
+ }
+ }
+
+ .right-panel {
+ display: flex;
+ justify-content: space-between;
+ width: 50%;
+
+ @include xs-to-md {
+ width: 100%;
+ display: flex;
+ }
+
+ @include xs-to-sm {
+ display: flex;
+ }
+ }
+
+ .challenge-details {
+ display: inline-block;
+ vertical-align: baseline;
+ width: 82%;
+ margin-right: $challenge-space-30;
+
+ @include md {
+ margin-right: 180px;
+ }
+
+ @include xs-to-sm {
+ margin-right: 0;
+ }
+
+ a {
+ line-height: $challenge-space-20;
+
+ @include xs-to-sm {
+ display: inline-block;
+ }
+ }
+ }
+
+ .type {
+ width: 40%;
+ }
+
+ .status {
+ width: 30%;
+ }
+
+ .numHours {
+ width: 30%;
+ }
+}
diff --git a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx
index 075eb87c4..d3462c7ab 100644
--- a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx
+++ b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx
@@ -53,6 +53,7 @@ export default function FiltersPanel({
isAuth,
auth,
isReviewOpportunitiesBucket,
+ isCopilotOpportunitiesBucket,
activeBucket,
onClose,
// onSaveFilter,
@@ -216,6 +217,7 @@ export default function FiltersPanel({
/>
+ { !isCopilotOpportunitiesBucket && (
@@ -257,8 +259,9 @@ export default function FiltersPanel({
+ )}
- { !isReviewOpportunitiesBucket
+ { !isReviewOpportunitiesBucket && !isCopilotOpportunitiesBucket
&& (
@@ -456,7 +459,7 @@ export default function FiltersPanel({
)
}
-
+ { !isCopilotOpportunitiesBucket && (
+ )}
);
}
@@ -504,6 +508,7 @@ FiltersPanel.defaultProps = {
isAuth: false,
// isSavingFilter: false,
isReviewOpportunitiesBucket: false,
+ isCopilotOpportunitiesBucket: false,
// onSaveFilter: _.noop,
onClose: _.noop,
expanding: false,
@@ -520,6 +525,7 @@ FiltersPanel.propTypes = {
auth: PT.shape().isRequired,
// isSavingFilter: PT.bool,
isReviewOpportunitiesBucket: PT.bool,
+ isCopilotOpportunitiesBucket: PT.bool,
// onSaveFilter: PT.func,
selectCommunity: PT.func.isRequired,
// selectedCommunityId: PT.string.isRequired,
diff --git a/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx
new file mode 100644
index 000000000..09b4c4680
--- /dev/null
+++ b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx
@@ -0,0 +1,136 @@
+import _ from 'lodash';
+import PT from 'prop-types';
+import React from 'react';
+import Sort from 'utils/challenge-listing/sort';
+import { BUCKET_DATA } from 'utils/challenge-listing/buckets';
+import SortingSelectBar from 'components/SortingSelectBar';
+import Waypoint from 'react-waypoint';
+
+import CopilotOpportunityHeader from 'components/challenge-listing/CopilotOpportunityHeader';
+import CardPlaceholder from '../../placeholders/ChallengeCard';
+import CopilotOpportunityCard from '../../CopilotOpportunityCard';
+
+import './style.scss';
+
+
+const NO_RESULTS_MESSAGE = 'No copilot opportunities found';
+const LOADING_MESSAGE = 'Loading Copilot Opportunities';
+
+export default function CopilotOpportunityBucket({
+ bucket,
+ challengesUrl,
+ expandedTags,
+ expandTag,
+ keepPlaceholders,
+ needLoad,
+ loading,
+ loadMore,
+ opportunities,
+ setFilterState,
+ setSort,
+ sort,
+ setSearchText,
+}) {
+ if (!opportunities.length && !loadMore) return null;
+
+ const activeSort = sort || BUCKET_DATA[bucket].sorts[0];
+
+ const sortedOpportunities = _.clone(opportunities);
+ sortedOpportunities.sort(Sort[activeSort].func);
+
+ const filteredOpportunities = sortedOpportunities;
+
+ const cards = filteredOpportunities.map(item => (
+ {
+ setFilterState({ search: tag });
+ setSearchText(tag);
+ }}
+ opportunity={item}
+ key={item.id}
+ />
+ ));
+
+ const placeholders = [];
+ if ((loading || keepPlaceholders) && cards.length === 0) {
+ for (let i = 0; i < 10; i += 1) {
+ placeholders.push();
+ }
+ }
+
+ return (
+
+ {filteredOpportunities.length > 0 && (
+
({
+ label: Sort[item].name,
+ value: item,
+ }))}
+ value={{
+ label: Sort[activeSort].name,
+ value: activeSort,
+ }}
+ onSelect={setSort}
+ />
+ )}
+
+ {cards}
+ {!loading && filteredOpportunities.length === 0 && (
+
+
+ ({
+ label: Sort[item].name,
+ value: item,
+ }))}
+ value={{
+ label: Sort[activeSort].name,
+ value: activeSort,
+ }}
+ onSelect={setSort}
+ />
+
+ {needLoad ? LOADING_MESSAGE : NO_RESULTS_MESSAGE}
+
+
+
+ )}
+ {loadMore && !loading ? (
+
+ ) : null}
+ {placeholders}
+
+ );
+}
+
+CopilotOpportunityBucket.defaultProps = {
+ expandedTags: [],
+ expandTag: null,
+ keepPlaceholders: false,
+ needLoad: false,
+ loading: false,
+ loadMore: null,
+ sort: null,
+};
+
+CopilotOpportunityBucket.propTypes = {
+ bucket: PT.string.isRequired,
+ challengesUrl: PT.string.isRequired,
+ expandedTags: PT.arrayOf(PT.number),
+ expandTag: PT.func,
+ // filterState: PT.shape().isRequired,
+ opportunities: PT.arrayOf(PT.shape()).isRequired,
+ keepPlaceholders: PT.bool,
+ needLoad: PT.bool,
+ loading: PT.bool,
+ loadMore: PT.func,
+ setFilterState: PT.func.isRequired,
+ setSort: PT.func.isRequired,
+ sort: PT.string,
+ setSearchText: PT.func.isRequired,
+};
diff --git a/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/style.scss b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/style.scss
new file mode 100644
index 000000000..8ef8374d1
--- /dev/null
+++ b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/style.scss
@@ -0,0 +1,29 @@
+@import "~styles/mixins";
+
+.copilot-opportunity-bucket {
+ background: $tc-white;
+ margin: 20px 0;
+
+ .no-results {
+ @include tc-label-lg;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: $tc-white;
+
+ @include roboto-regular;
+
+ font-size: 24px;
+ line-height: 32px;
+ color: $tco-black;
+ padding-top: 80px;
+ border-top: 2px solid $listing-gray;
+ border-radius: 1px;
+ margin-left: 24px;
+
+ @include xs-to-md {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/src/shared/components/challenge-listing/Listing/index.jsx b/src/shared/components/challenge-listing/Listing/index.jsx
index badc68173..983a1e587 100644
--- a/src/shared/components/challenge-listing/Listing/index.jsx
+++ b/src/shared/components/challenge-listing/Listing/index.jsx
@@ -7,12 +7,13 @@ import React from 'react';
import PT from 'prop-types';
import { connect } from 'react-redux';
import {
- BUCKETS, isReviewOpportunitiesBucket, NO_LIVE_CHALLENGES_CONFIG,
+ BUCKETS, isReviewOpportunitiesBucket, isCopilotOpportunitiesBucket, NO_LIVE_CHALLENGES_CONFIG,
// BUCKETS, getBuckets, isReviewOpportunitiesBucket, NO_LIVE_CHALLENGES_CONFIG,
} from 'utils/challenge-listing/buckets';
// import { challenge as challengeUtils } from 'topcoder-react-lib';
import Bucket from './Bucket';
import ReviewOpportunityBucket from './ReviewOpportunityBucket';
+import CopilotOpportunityBucket from './CopilotOpportunityBucket';
import CardPlaceholder from '../placeholders/ChallengeCard';
import './style.scss';
@@ -29,6 +30,7 @@ function Listing({
allPastChallengesLoaded,
allOpenForRegistrationChallengesLoaded,
challenges,
+ copilotOpportunities,
openForRegistrationChallenges,
myChallenges,
myPastChallenges,
@@ -42,6 +44,8 @@ function Listing({
filterState,
keepPastPlaceholders,
needLoad,
+ loadingCopilotOpportunities,
+ loadMoreCopilotOpportunities,
loadingPastChallenges,
loadingReviewOpportunities,
loadingMyChallenges,
@@ -141,70 +145,91 @@ function Listing({
default:
break;
}
- return (
- /* Review Opportunities use a different Bucket, Card and data source than normal challenges
+
+ /* Review Opportunities use a different Bucket, Card and data source than normal challenges
* and are only shown when explicitly chosen from the sidebar */
- isReviewOpportunitiesBucket(bucket)
- ? (
- setSort(bucket, sort)}
- sort={sorts[bucket]}
- challengeTypes={challengeTypes}
- isLoggedIn={isLoggedIn}
- setSearchText={setSearchText}
- />
- )
- : (
- {
- selectBucket(bucket);
- loadMore();
- }}
- expanded={newExpanded}
- expanding={expanding}
- expandedTags={expandedTags}
- expandTag={expandTag}
- filterState={filterState}
- // keepPlaceholders={keepPlaceholders}
- needLoad={needLoad}
- loading={loading}
- loadMore={loadMore}
- newChallengeDetails={newChallengeDetails}
- openChallengesInNewTabs={openChallengesInNewTabs}
- prizeMode={prizeMode}
- selectChallengeDetailsTab={selectChallengeDetailsTab}
- selectedCommunityId={selectedCommunityId}
- setFilterState={setFilterState}
- setSort={sort => setSort(bucket, sort)}
- sort={sorts[bucket]}
- userId={_.get(auth, 'user.userId')}
- auth={auth}
- activeBucket={activeBucket}
- // searchTimestamp={searchTimestamp}
- isLoggedIn={isLoggedIn}
- setSearchText={setSearchText}
- />
- )
- );
+ let content;
+
+ if (isReviewOpportunitiesBucket(bucket)) {
+ content = (
+ setSort(bucket, sort)}
+ sort={sorts[bucket]}
+ challengeTypes={challengeTypes}
+ isLoggedIn={isLoggedIn}
+ setSearchText={setSearchText}
+ />
+ );
+ } else if (isCopilotOpportunitiesBucket(bucket)) {
+ content = (
+ setSort(bucket, sort)}
+ sort={sorts[bucket]}
+ challengeTypes={challengeTypes}
+ isLoggedIn={isLoggedIn}
+ setSearchText={setSearchText}
+ />
+ );
+ } else {
+ content = (
+ {
+ selectBucket(bucket);
+ loadMore();
+ }}
+ expanded={newExpanded}
+ expanding={expanding}
+ expandedTags={expandedTags}
+ expandTag={expandTag}
+ filterState={filterState}
+ needLoad={needLoad}
+ loading={loading}
+ loadMore={loadMore}
+ newChallengeDetails={newChallengeDetails}
+ openChallengesInNewTabs={openChallengesInNewTabs}
+ prizeMode={prizeMode}
+ selectChallengeDetailsTab={selectChallengeDetailsTab}
+ selectedCommunityId={selectedCommunityId}
+ setFilterState={setFilterState}
+ setSort={sort => setSort(bucket, sort)}
+ sort={sorts[bucket]}
+ userId={_.get(auth, 'user.userId')}
+ auth={auth}
+ activeBucket={activeBucket}
+ isLoggedIn={isLoggedIn}
+ setSearchText={setSearchText}
+ />
+ );
+ }
+
+ return content;
};
if ((activeBucket !== BUCKETS.SAVED_FILTER)) {
@@ -276,6 +301,7 @@ function Listing({
Listing.defaultProps = {
challenges: [],
+ copilotOpportunities: [],
openForRegistrationChallenges: [],
myChallenges: [],
myPastChallenges: [],
@@ -290,6 +316,7 @@ Listing.defaultProps = {
// extraBucket: null,
loadMorePast: null,
loadMoreReviewOpportunities: null,
+ loadMoreCopilotOpportunities: null,
loadMoreMy: null,
loadMoreMyPast: null,
loadMoreAll: null,
@@ -335,6 +362,9 @@ Listing.propTypes = {
filterState: PT.shape().isRequired,
keepPastPlaceholders: PT.bool.isRequired,
needLoad: PT.bool.isRequired,
+ loadingCopilotOpportunities: PT.bool.isRequired,
+ loadMoreCopilotOpportunities: PT.func,
+ copilotOpportunities: PT.arrayOf(PT.shape()),
loadingPastChallenges: PT.bool.isRequired,
loadingMyChallenges: PT.bool.isRequired,
loadingMyPastChallenges: PT.bool.isRequired,
@@ -377,6 +407,7 @@ const mapStateToProps = (state) => {
allChallengesLoaded: cl.allChallengesLoaded,
allPastChallengesLoaded: cl.allPastChallengesLoaded,
allOpenForRegistrationChallengesLoaded: cl.allOpenForRegistrationChallengesLoaded,
+ // allCopilotOpportunitiesLoaded: cl.allCopilotOpportunitiesLoaded,
// pastSearchTimestamp: cl.pastSearchTimestamp,
challengeTypes: cl.challengeTypes,
};
diff --git a/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx b/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx
index cdcd59930..8b8adfca1 100644
--- a/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx
+++ b/src/shared/components/challenge-listing/Sidebar/BucketSelector/index.jsx
@@ -90,6 +90,7 @@ export default function BucketSelector({
{/* DISABLED: Until api receive fix community-app#5073 */}
{/* {getBucket(BUCKETS.ONGOING)} */}
{getBucket(BUCKETS.REVIEW_OPPORTUNITIES)}
+ {getBucket(BUCKETS.COPILOT_OPPORTUNITIES)}
{/* {getBucket(BUCKETS.PAST)} */}
{/* NOTE: We do not show upcoming challenges for now, for various reasons,
* more political than technical ;)
diff --git a/src/shared/components/challenge-listing/index.jsx b/src/shared/components/challenge-listing/index.jsx
index 7a4a6c365..76be6af6d 100644
--- a/src/shared/components/challenge-listing/index.jsx
+++ b/src/shared/components/challenge-listing/index.jsx
@@ -108,6 +108,7 @@ export default function ChallengeListing(props) {
activeBucket={activeBucket}
auth={props.auth}
challenges={challenges}
+ copilotOpportunities={props.copilotOpportunities}
openForRegistrationChallenges={openForRegistrationChallenges}
myChallenges={myChallenges}
myPastChallenges={myPastChallenges}
@@ -125,12 +126,14 @@ export default function ChallengeListing(props) {
loadingMyChallenges={props.loadingMyChallenges}
loadingMyPastChallenges={props.loadingMyPastChallenges}
loadingAllChallenges={props.loadingAllChallenges}
+ loadingCopilotOpportunities={props.loadingCopilotOpportunities}
loadingOpenForRegistrationChallenges={props.loadingOpenForRegistrationChallenges}
loadingOnGoingChallenges={props.loadingOnGoingChallenges}
loadingReviewOpportunities={props.loadingReviewOpportunities}
loadMoreMy={props.loadMoreMy}
loadMoreMyPast={props.loadMoreMyPast}
loadMoreAll={props.loadMoreAll}
+ loadMoreCopilotOpportunities={props.loadMoreCopilotOpportunities}
loadMoreOpenForRegistration={props.loadMoreOpenForRegistration}
loadMoreOnGoing={props.loadMoreOnGoing}
loadMorePast={props.loadMorePast}
@@ -200,11 +203,13 @@ ChallengeListing.defaultProps = {
auth: null,
// communityFilter: null,
communityName: null,
+ copilotOpportunities: [],
// extraBucket: null,
// hideTcLinksInFooter: false,
loadMoreMy: null,
loadMoreMyPast: null,
loadMoreAll: null,
+ loadMoreCopilotOpportunities: null,
loadMoreOpenForRegistration: null,
loadMoreOnGoing: null,
loadMorePast: null,
@@ -230,6 +235,7 @@ ChallengeListing.propTypes = {
activeBucket: PT.string.isRequired,
expanding: PT.bool,
challenges: PT.arrayOf(PT.shape()).isRequired,
+ copilotOpportunities: PT.arrayOf(PT.shape()),
openForRegistrationChallenges: PT.arrayOf(PT.shape()).isRequired,
myChallenges: PT.arrayOf(PT.shape()).isRequired,
myPastChallenges: PT.arrayOf(PT.shape()).isRequired,
@@ -252,6 +258,7 @@ ChallengeListing.propTypes = {
loadingMyChallenges: PT.bool.isRequired,
loadingMyPastChallenges: PT.bool.isRequired,
loadingAllChallenges: PT.bool.isRequired,
+ loadingCopilotOpportunities: PT.bool.isRequired,
loadingOpenForRegistrationChallenges: PT.bool.isRequired,
loadingOnGoingChallenges: PT.bool.isRequired,
loadingPastChallenges: PT.bool.isRequired,
@@ -259,6 +266,7 @@ ChallengeListing.propTypes = {
loadMoreMy: PT.func,
loadMoreMyPast: PT.func,
loadMoreAll: PT.func,
+ loadMoreCopilotOpportunities: PT.func,
loadMoreOpenForRegistration: PT.func,
loadMoreOnGoing: PT.func,
loadMorePast: PT.func,
diff --git a/src/shared/containers/challenge-listing/ChallengeSearchBar.jsx b/src/shared/containers/challenge-listing/ChallengeSearchBar.jsx
index b3f334405..3e8c412c5 100644
--- a/src/shared/containers/challenge-listing/ChallengeSearchBar.jsx
+++ b/src/shared/containers/challenge-listing/ChallengeSearchBar.jsx
@@ -7,7 +7,7 @@ import challengeListingActions from 'actions/challenge-listing';
import ChallengeSearchBar from 'components/challenge-listing/Filters/ChallengeSearchBar';
import PT from 'prop-types';
import React from 'react';
-import { isReviewOpportunitiesBucket, isPastBucket } from 'utils/challenge-listing/buckets';
+import { isReviewOpportunitiesBucket, isPastBucket, isCopilotOpportunitiesBucket } from 'utils/challenge-listing/buckets';
import { connect } from 'react-redux';
import _ from 'lodash';
@@ -34,17 +34,21 @@ export class Container extends React.Component {
} = this.props;
const isForReviewOpportunities = isReviewOpportunitiesBucket(activeBucket);
+ const isForCopilotOpportunities = isCopilotOpportunitiesBucket(activeBucket);
const searchPlaceholderText = isPastBucket(activeBucket) ? 'Search Past Challenges' : 'Search active';
return (
- {
- setSearchText(text);
- this.onSearch(text);
- }}
- placeholder={isForReviewOpportunities ? 'Search Review Opportunities' : searchPlaceholderText}
- query={searchText}
- />
+ !isForCopilotOpportunities
+ ? (
+ {
+ setSearchText(text);
+ this.onSearch(text);
+ }}
+ placeholder={isForReviewOpportunities ? 'Search Review Opportunities' : searchPlaceholderText}
+ query={searchText}
+ />
+ ) : null
);
}
}
diff --git a/src/shared/containers/challenge-listing/FilterPanel.jsx b/src/shared/containers/challenge-listing/FilterPanel.jsx
index bdbcf4381..36a342925 100644
--- a/src/shared/containers/challenge-listing/FilterPanel.jsx
+++ b/src/shared/containers/challenge-listing/FilterPanel.jsx
@@ -11,7 +11,7 @@ import FilterPanel from 'components/challenge-listing/Filters/FiltersPanel';
import PT from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
-import { BUCKETS, isReviewOpportunitiesBucket } from 'utils/challenge-listing/buckets';
+import { BUCKETS, isReviewOpportunitiesBucket, isCopilotOpportunitiesBucket } from 'utils/challenge-listing/buckets';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import qs from 'qs';
@@ -132,6 +132,7 @@ export class Container extends React.Component {
];
const isForReviewOpportunities = isReviewOpportunitiesBucket(activeBucket);
+ const isForCopilotOpportunities = isCopilotOpportunitiesBucket(activeBucket);
const filterPanel = (
getCopilotOpportunities(
+ 1 + lastRequestedPageOfCopilotOpportunities,
+ );
+ }
+
let communityFilter = communityFilters.find(item => item.communityId === selectedCommunityId);
if (communityFilter) communityFilter = communityFilter.challengeFilter;
@@ -649,6 +661,7 @@ export class ListingContainer extends React.Component {
challengesUrl={challengesUrl}
communityFilter={communityFilter}
communityName={communityName}
+ copilotOpportunities={copilotOpportunities}
defaultCommunityId={defaultCommunityId}
expanding={expanding}
expandedTags={expandedTags}
@@ -661,6 +674,7 @@ export class ListingContainer extends React.Component {
// lastUpdateOfActiveChallenges={lastUpdateOfActiveChallenges}
// eslint-disable-next-line max-len
needLoad={needLoad}
+ loadingCopilotOpportunities={Boolean(loadingCopilotOpportunitiesUUID)}
loadingMyChallenges={Boolean(loadingMyChallengesUUID)}
loadingMyPastChallenges={Boolean(loadingMyPastChallengesUUID)}
loadingAllChallenges={Boolean(loadingAllChallengesUUID)}
@@ -679,6 +693,7 @@ export class ListingContainer extends React.Component {
selectedCommunityId={selectedCommunityId}
loadMorePast={loadMorePast}
loadMoreReviewOpportunities={loadMoreReviewOpportunities}
+ loadMoreCopilotOpportunities={loadMoreCopilotOpportunities}
loadMoreMy={loadMoreMy}
loadMoreMyPast={loadMoreMyPast}
loadMoreAll={loadMoreAll}
@@ -750,6 +765,7 @@ ListingContainer.propTypes = {
}).isRequired,
// allActiveChallengesLoaded: PT.bool.isRequired,
// allPastChallengesLoaded: PT.bool.isRequired,
+ allCopilotOpportunitiesLoaded: PT.bool.isRequired,
allReviewOpportunitiesLoaded: PT.bool.isRequired,
ChallengeListingBanner: PT.node,
challenges: PT.arrayOf(PT.shape({})).isRequired, // active challenges.
@@ -768,6 +784,7 @@ ListingContainer.propTypes = {
loadingUuid: PT.string.isRequired,
timestamp: PT.number.isRequired,
}).isRequired,
+ copilotOpportunities: PT.arrayOf(PT.shape()).isRequired,
defaultCommunityId: PT.string,
dropChallenges: PT.func.isRequired,
dropMyChallenges: PT.func.isRequired,
@@ -792,6 +809,7 @@ ListingContainer.propTypes = {
getCommunitiesList: PT.func.isRequired,
getPastChallenges: PT.func.isRequired,
getReviewOpportunities: PT.func.isRequired,
+ getCopilotOpportunities: PT.func.isRequired,
keepPastPlaceholders: PT.bool.isRequired,
lastRequestedPageOfActiveChallenges: PT.number.isRequired,
lastRequestedPageOfOpenForRegistrationChallenges: PT.number.isRequired,
@@ -800,8 +818,10 @@ ListingContainer.propTypes = {
lastRequestedPageOfAllChallenges: PT.number.isRequired,
lastRequestedPageOfPastChallenges: PT.number.isRequired,
lastRequestedPageOfReviewOpportunities: PT.number.isRequired,
+ lastRequestedPageOfCopilotOpportunities: PT.number.isRequired,
// lastUpdateOfActiveChallenges: PT.number.isRequired,
loadingActiveChallengesUUID: PT.string.isRequired,
+ loadingCopilotOpportunitiesUUID: PT.string.isRequired,
loadingOpenForRegistrationChallengesUUID: PT.string.isRequired,
loadingMyChallengesUUID: PT.string.isRequired,
loadingMyPastChallengesUUID: PT.string.isRequired,
@@ -848,10 +868,12 @@ const mapStateToProps = (state, ownProps) => {
return {
auth: state.auth,
// allActiveChallengesLoaded: cl.allActiveChallengesLoaded,
+ allCopilotOpportunitiesLoaded: cl.allCopilotOpportunitiesLoaded,
allPastChallengesLoaded: cl.allPastChallengesLoaded,
allReviewOpportunitiesLoaded: cl.allReviewOpportunitiesLoaded,
filter: cl.filter,
challenges: cl.challenges,
+ copilotOpportunities: cl.copilotOpportunities,
openForRegistrationChallenges: cl.openForRegistrationChallenges,
myChallenges: cl.myChallenges,
myPastChallenges: cl.myPastChallenges,
@@ -872,8 +894,10 @@ const mapStateToProps = (state, ownProps) => {
lastRequestedPageOfAllChallenges: cl.lastRequestedPageOfAllChallenges,
lastRequestedPageOfPastChallenges: cl.lastRequestedPageOfPastChallenges,
lastRequestedPageOfReviewOpportunities: cl.lastRequestedPageOfReviewOpportunities,
+ lastRequestedPageOfCopilotOpportunities: cl.lastRequestedPageOfCopilotOpportunities,
// lastUpdateOfActiveChallenges: cl.lastUpdateOfActiveChallenges,
loadingActiveChallengesUUID: cl.loadingActiveChallengesUUID,
+ loadingCopilotOpportunitiesUUID: cl.loadingCopilotOpportunitiesUUID,
loadingOpenForRegistrationChallengesUUID: cl.loadingOpenForRegistrationChallengesUUID,
loadingMyChallengesUUID: cl.loadingMyChallengesUUID,
loadingMyPastChallengesUUID: cl.loadingMyPastChallengesUUID,
@@ -898,7 +922,8 @@ const mapStateToProps = (state, ownProps) => {
loading: Boolean(cl.loadingActiveChallengesUUID)
|| Boolean(cl.loadingOpenForRegistrationChallengesUUID)
|| Boolean(cl.loadingMyChallengesUUID) || Boolean(cl.loadingAllChallengesUUID)
- || Boolean(cl.loadingPastChallengesUUID) || cl.loadingReviewOpportunitiesUUID,
+ || Boolean(cl.loadingPastChallengesUUID) || cl.loadingReviewOpportunitiesUUID
+ || cl.loadingCopilotOpportunitiesUUID,
};
};
@@ -981,6 +1006,11 @@ function mapDispatchToProps(dispatch) {
dispatch(a.getReviewOpportunitiesInit(uuid, page));
dispatch(a.getReviewOpportunitiesDone(uuid, page, token));
},
+ getCopilotOpportunities: (page) => {
+ const uuid = shortId();
+ dispatch(a.getCopilotOpportunitiesInit(uuid, page));
+ dispatch(a.getCopilotOpportunitiesDone(uuid, page));
+ },
selectBucket: (bucket, expanding) => dispatch(sa.selectBucket(bucket, expanding)),
selectBucketDone: () => dispatch(sa.selectBucketDone()),
selectChallengeDetailsTab:
diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js
index add5ae894..2719e9040 100644
--- a/src/shared/reducers/challenge-listing/index.js
+++ b/src/shared/reducers/challenge-listing/index.js
@@ -495,6 +495,48 @@ function onGetReviewOpportunitiesDone(state, { payload, error }) {
};
}
+/**
+ * Handles CHALLENGE_LISTING/GET_COPILOT_OPPORTUNITIES_INIT action.
+ * @param {Object} state
+ * @param {Object} action Payload will be page, uuid
+ * @return {Object} New state
+ */
+function onGetCopilotOpportunitiesInit(state, { payload }) {
+ return {
+ ...state,
+ lastRequestedPageOfCopilotOpportunities: payload.page,
+ loadingCopilotOpportunitiesUUID: payload.uuid,
+ };
+}
+
+/**
+ * Handles CHALLENGE_LISTING/GET_COPILOT_OPPORTUNITIES_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from API call and UUID
+ * @return {Object} New state
+ */
+function onGetCopilotOpportunitiesDone(state, { payload, error }) {
+ if (error) return state;
+
+ const { uuid, loaded } = payload;
+
+ if (uuid !== state.loadingCopilotOpportunitiesUUID) return state;
+
+ const ids = new Set();
+ loaded.forEach(item => ids.add(item.id));
+ const copilotOpportunities = state.copilotOpportunities
+ .filter(item => !ids.has(item.id))
+ .concat(loaded);
+
+ return {
+ ...state,
+ copilotOpportunities,
+ loadingCopilotOpportunitiesUUID: '',
+ allCopilotOpportunitiesLoaded: loaded.length === 0,
+ };
+}
+
+
/**
* Inits the loading of SRMs.
* @param {Object} state
@@ -689,6 +731,7 @@ function create(initialState) {
myPastChallenges: [],
openForRegistrationChallenges: [],
pastChallenges: [],
+ // copilotOpportunities: [],
lastRequestedPageOfActiveChallenges: -1,
lastRequestedPageOfOpenForRegistrationChallenges: -1,
lastRequestedPageOfMyChallenges: -1,
@@ -696,6 +739,7 @@ function create(initialState) {
lastRequestedPageOfAllChallenges: -1,
lastRequestedPageOfRecommendedChallenges: -1,
lastRequestedPageOfPastChallenges: -1,
+ // lastRequestedPageOfCopilotOpportunities: -1,
// lastRequestedPageOfReviewOpportunities: -1,
// lastUpdateOfActiveChallenges: 0,
loadingActiveChallengesUUID: '',
@@ -821,6 +865,9 @@ function create(initialState) {
[a.getReviewOpportunitiesInit]: onGetReviewOpportunitiesInit,
[a.getReviewOpportunitiesDone]: onGetReviewOpportunitiesDone,
+ [a.getCopilotOpportunitiesInit]: onGetCopilotOpportunitiesInit,
+ [a.getCopilotOpportunitiesDone]: onGetCopilotOpportunitiesDone,
+
[a.getSrmsInit]: onGetSrmsInit,
[a.getSrmsDone]: onGetSrmsDone,
@@ -846,6 +893,7 @@ function create(initialState) {
allRecommendedChallengesLoaded: false,
allPastChallengesLoaded: false,
allReviewOpportunitiesLoaded: false,
+ allCopilotOpportunitiesLoaded: false,
challenges: [],
allChallenges: [],
@@ -857,6 +905,7 @@ function create(initialState) {
challengeTypes: [],
challengeTypesMap: {},
challengeTags: [],
+ copilotOpportunities: [],
expandedTags: [],
@@ -870,6 +919,7 @@ function create(initialState) {
lastRequestedPageOfMyPastChallenges: -1,
lastRequestedPageOfPastChallenges: -1,
lastRequestedPageOfReviewOpportunities: -1,
+ lastRequestedPageOfCopilotOpportunities: -1,
// lastUpdateOfActiveChallenges: 0,
loadingActiveChallengesUUID: '',
@@ -913,6 +963,7 @@ function create(initialState) {
all: 'startDate',
// past: 'updated',
reviewOpportunities: 'review-opportunities-start-date',
+ copilotOpportunities: 'copilot-opportunities-start-date',
allPast: 'startDate',
myPast: 'startDate',
},
diff --git a/src/shared/services/copilotOpportunities.js b/src/shared/services/copilotOpportunities.js
new file mode 100644
index 000000000..2bfadd7b1
--- /dev/null
+++ b/src/shared/services/copilotOpportunities.js
@@ -0,0 +1,19 @@
+import { config } from 'topcoder-react-utils';
+
+const v5ApiUrl = config.API.V5;
+
+/**
+ * Fetches copilot opportunities.
+ *
+ * @param {number} page - Page number (1-based).
+ * @param {number} pageSize - Number of items per page.
+ * @param {string} sort - Sort order (e.g., 'createdAt desc').
+ * @returns {Promise