From e5cf98f4c52d12fdadeae425b9ac8be1c1c057d0 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 15 May 2025 20:39:02 +0530 Subject: [PATCH 01/11] feat(challenge-feed): pm-1188 copilot opportunities on challenge feed add support for fetching and displaying copilot opportunities within the challenge feed, including dynamic sorting and pagination handling. refs pm-1188 --- config/backup-default.js | 1 + config/default.js | 1 + src/shared/actions/challenge-listing/index.js | 21 ++ .../CopilotOpportunityCard/index.jsx | 77 ++++++ .../CopilotOpportunityCard/style.scss | 240 ++++++++++++++++++ .../CopilotOpportunityHeader/index.jsx | 33 +++ .../CopilotOpportunityHeader/style.scss | 93 +++++++ .../Filters/FiltersPanel/index.jsx | 10 +- .../CopilotOpportunityBucket/index.jsx | 147 +++++++++++ .../CopilotOpportunityBucket/style.scss | 29 +++ .../challenge-listing/Listing/index.jsx | 159 +++++++----- .../Sidebar/BucketSelector/index.jsx | 1 + .../components/challenge-listing/index.jsx | 8 + .../challenge-listing/FilterPanel.jsx | 4 +- .../challenge-listing/Listing/index.jsx | 32 ++- .../reducers/challenge-listing/index.js | 50 ++++ src/shared/services/copilotOpportunities.js | 19 ++ src/shared/utils/challenge-listing/buckets.js | 15 ++ src/shared/utils/challenge-listing/sort.js | 27 ++ 19 files changed, 899 insertions(+), 68 deletions(-) create mode 100644 src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx create mode 100644 src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss create mode 100644 src/shared/components/challenge-listing/CopilotOpportunityHeader/index.jsx create mode 100644 src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss create mode 100644 src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx create mode 100644 src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/style.scss create mode 100644 src/shared/services/copilotOpportunities.js diff --git a/config/backup-default.js b/config/backup-default.js index 2ea3ca1ee5..57d4eb617b 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 97d6e12835..1b8eeecdf8 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/src/shared/actions/challenge-listing/index.js b/src/shared/actions/challenge-listing/index.js index f7d109ca89..7d7784007c 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 0000000000..7d2f5d4e49 --- /dev/null +++ b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx @@ -0,0 +1,77 @@ +/** + * Component for rendering a Copilot Opportunity and associated Challenge + * information. Will be contained within a Bucket. + */ +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: 'DataScience', + qa: 'Quality Assurance', +}; + +function CopilotOpportunityCard({ + opportunity, +}) { + const skills = useMemo(() => _.uniq((opportunity.skills || []).map(skill => skill.name)), [ + opportunity.skills, + ]); + const start = moment(opportunity.startDate); + + return ( +
+
+ +
+ + {opportunity.project.name} + + +
+ + Starts {start.format('MMM DD')} + + { skills.length > 0 + && ( + + ) } +
+
+
+ +
+
+ {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 0000000000..749bbc8439 --- /dev/null +++ b/src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss @@ -0,0 +1,240 @@ +@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 0000000000..c458041691 --- /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 0000000000..286f3e60ba --- /dev/null +++ b/src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss @@ -0,0 +1,93 @@ +@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 075eb87c46..d3462c7abf 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 0000000000..f4972648df --- /dev/null +++ b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx @@ -0,0 +1,147 @@ +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 { challenge as challengeUtils } from 'topcoder-react-lib'; +import CopilotOpportunityHeader from 'components/challenge-listing/CopilotOpportunityHeader'; +import CardPlaceholder from '../../placeholders/ChallengeCard'; +import CopilotOpportunityCard from '../../CopilotOpportunityCard'; // <== Replace with your actual Copilot Card component + +import './style.scss'; + +// const Filter = challengeUtils.filter; + +const NO_RESULTS_MESSAGE = 'No copilot opportunities found'; +const LOADING_MESSAGE = 'Loading Copilot Opportunities'; + +// Functional implementation of CopilotOpportunityBucket component +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.filter( + // Filter.getReviewOpportunitiesFilterFunction({ + // ...BUCKET_DATA[bucket].filter, + // ...filterState, + // }, challengeTypes), + // ); + + 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, + // challengeTypes: PT.arrayOf(PT.shape()).isRequired, + 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 0000000000..72bbbca6e4 --- /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; + } + } +} \ No newline at end of file diff --git a/src/shared/components/challenge-listing/Listing/index.jsx b/src/shared/components/challenge-listing/Listing/index.jsx index badc681731..983a1e587b 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 cdcd599307..8b8adfca1d 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 7a4a6c3655..76be6af6d1 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/FilterPanel.jsx b/src/shared/containers/challenge-listing/FilterPanel.jsx index bdbcf43812..36a342925f 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 add5ae8941..394375cd55 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, @@ -821,6 +864,9 @@ function create(initialState) { [a.getReviewOpportunitiesInit]: onGetReviewOpportunitiesInit, [a.getReviewOpportunitiesDone]: onGetReviewOpportunitiesDone, + [a.getCopilotOpportunitiesInit]: onGetCopilotOpportunitiesInit, + [a.getCopilotOpportunitiesDone]: onGetCopilotOpportunitiesDone, + [a.getSrmsInit]: onGetSrmsInit, [a.getSrmsDone]: onGetSrmsDone, @@ -846,6 +892,7 @@ function create(initialState) { allRecommendedChallengesLoaded: false, allPastChallengesLoaded: false, allReviewOpportunitiesLoaded: false, + allCopilotOpportunitiesLoaded: false, challenges: [], allChallenges: [], @@ -857,6 +904,7 @@ function create(initialState) { challengeTypes: [], challengeTypesMap: {}, challengeTags: [], + copilotOpportunities: [], expandedTags: [], @@ -870,6 +918,7 @@ function create(initialState) { lastRequestedPageOfMyPastChallenges: -1, lastRequestedPageOfPastChallenges: -1, lastRequestedPageOfReviewOpportunities: -1, + lastRequestedPageOfCopilotOpportunities: -1, // lastUpdateOfActiveChallenges: 0, loadingActiveChallengesUUID: '', @@ -913,6 +962,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 0000000000..2bfadd7b1f --- /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} The fetched data. + */ +export default function getCopilotOpportunities(page, pageSize = 20, sort = 'createdAt desc') { + const url = `${v5ApiUrl}/projects/copilots/opportunities?page=${page}&pageSize=${pageSize}&sort=${encodeURIComponent(sort)}`; + + return fetch(url, { + method: 'GET', + }).then(res => res.json()); +} diff --git a/src/shared/utils/challenge-listing/buckets.js b/src/shared/utils/challenge-listing/buckets.js index 857662576a..527b316b2b 100644 --- a/src/shared/utils/challenge-listing/buckets.js +++ b/src/shared/utils/challenge-listing/buckets.js @@ -10,6 +10,7 @@ export const BUCKETS = { ALL: 'all', MY: 'my', OPEN_FOR_REGISTRATION: 'openForRegistration', + COPILOT_OPPORTUNITIES: 'copilotOpportunities', ONGOING: 'ongoing', PAST: 'past', // SAVED_FILTER: 'saved-filter', @@ -72,6 +73,15 @@ export const BUCKET_DATA = { SORTS.TITLE_A_TO_Z, ], }, + [BUCKETS.COPILOT_OPPORTUNITIES]: { + name: 'Copilot Opportunities', + sorts: [ + SORTS.COPILOT_OPPORTUNITIES_START_DATE, + SORTS.COPILOT_OPPORTUNITIES_STATUS, + SORTS.COPILOT_OPPORTUNITIES_TYPE, + SORTS.COPILOT_OPPORTUNITIES_TITLE_A_TO_Z, + ], + }, [BUCKETS.ONGOING]: { // filter: { // registrationOpen: false, @@ -149,6 +159,7 @@ export const NO_LIVE_CHALLENGES_CONFIG = { [BUCKETS.ALL]: 'No Live Challenges found', [BUCKETS.MY]: 'No challenges found', [BUCKETS.OPEN_FOR_REGISTRATION]: 'No challenges found', + [BUCKETS.COPILOT_OPPORTUNITIES]: 'No Copilot Opportunities found', [BUCKETS.ONGOING]: 'No challenges found', // [BUCKETS.PAST]: 'No challenges found in Past Challenges', // [BUCKETS.SAVED_FILTER]: 'No challenges found in Saved filter Challenges', @@ -177,6 +188,10 @@ export const isReviewOpportunitiesBucket = bucket => ( // bucket === BUCKETS.REVIEW_OPPORTUNITIES || bucket === BUCKETS.SAVED_REVIEW_OPPORTUNITIES_FILTER); bucket === BUCKETS.REVIEW_OPPORTUNITIES); +export const isCopilotOpportunitiesBucket = bucket => ( + bucket === BUCKETS.COPILOT_OPPORTUNITIES +); + /** * Registers a new bucket. * @param {String} id diff --git a/src/shared/utils/challenge-listing/sort.js b/src/shared/utils/challenge-listing/sort.js index 089cd519a7..094a4c6d45 100644 --- a/src/shared/utils/challenge-listing/sort.js +++ b/src/shared/utils/challenge-listing/sort.js @@ -24,6 +24,10 @@ export const SORTS = { REVIEW_OPPORTUNITIES_PAYMENT: 'review-opportunities-payment', REVIEW_OPPORTUNITIES_START_DATE: 'review-opportunities-start-date', BEST_MATCH: 'bestMatch', + COPILOT_OPPORTUNITIES_START_DATE: 'copilot-opportunities-start-date', + COPILOT_OPPORTUNITIES_TITLE_A_TO_Z: 'copilot-opportunities-title-a-to-z', + COPILOT_OPPORTUNITIES_STATUS: 'copilot-opportunities-status', + COPILOT_OPPORTUNITIES_TYPE: 'copilot-opportunities-type', }; export default { @@ -104,6 +108,29 @@ export default { func: (a, b) => moment(a.startDate) - moment(b.startDate), name: 'Review start date', }, + [SORTS.COPILOT_OPPORTUNITIES_STATUS]: { + func: (a, b) => a.status.localeCompare(b.status), + name: 'Status', + }, + [SORTS.COPILOT_OPPORTUNITIES_TITLE_A_TO_Z]: { + func: (a, b) => a.project.name.localeCompare(b.project.name), + name: 'Title A-Z', + }, + [SORTS.COPILOT_OPPORTUNITIES_TYPE]: { + func: (a, b) => a.type.localeCompare(b.type), + name: 'Type', + }, + [SORTS.COPILOT_OPPORTUNITIES_START_DATE]: { + func: (a, b) => { + const statusPriority = status => (status === 'active' ? 1 : 0); + // First: prioritize active over non-active + const statusDiff = statusPriority(b.status) - statusPriority(a.status); + if (statusDiff !== 0) return statusDiff; + // Then: sort by createdAt descending + return moment(b.createdAt) - moment(a.createdAt); + }, + name: 'Most recent opportunities', + }, [SORTS.BEST_MATCH]: { func: (a, b) => calculateScore(b.jaccard_index) - calculateScore(a.jaccard_index), name: 'Best Match', From 9a8d2876ba3ec6ebeb670212a0d0d3336d42fc37 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 15 May 2025 20:52:07 +0530 Subject: [PATCH 02/11] chore(styles): linting fixes in scss files apply consistent formatting and fix linting errors in all scss files to maintain code quality. no issue --- .../CopilotOpportunityCard/style.scss | 108 +++++++++--------- .../CopilotOpportunityHeader/style.scss | 3 - .../CopilotOpportunityBucket/style.scss | 2 +- 3 files changed, 53 insertions(+), 60 deletions(-) diff --git a/src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss b/src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss index 749bbc8439..162f32a83e 100644 --- a/src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss +++ b/src/shared/components/challenge-listing/CopilotOpportunityCard/style.scss @@ -134,7 +134,7 @@ $status-radius-4: $corner-radius * 2; width: 30%; &.completed { - color: $tc-orange; + color: $tc-orange; } @include xs-to-md { @@ -173,68 +173,64 @@ $status-radius-4: $corner-radius * 2; 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; - } - + 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; - } + 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; - } - + 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; - } + color: $tc-black; + font-weight: 500; + font-size: 14px; + line-height: 16px; + } } - - } - diff --git a/src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss b/src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss index 286f3e60ba..77716dc919 100644 --- a/src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss +++ b/src/shared/components/challenge-listing/CopilotOpportunityHeader/style.scss @@ -87,7 +87,4 @@ $challenge-space-30: $base-unit * 6; .numHours { width: 30%; } - - } - diff --git a/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/style.scss b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/style.scss index 72bbbca6e4..8ef8374d1f 100644 --- a/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/style.scss +++ b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/style.scss @@ -26,4 +26,4 @@ margin-left: 0; } } -} \ No newline at end of file +} From f83c121973a998a5fc5301452f39cf23fd099c84 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 19 May 2025 09:25:41 +0300 Subject: [PATCH 03/11] test: fix snaphsots --- .../SubmissionHistoryRow/__snapshots__/index.jsx.snap | 2 +- .../components/challenge-listing/__snapshots__/index.jsx.snap | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) 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 index 59f37d8336..4716dec9ce 100644 --- 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 @@ -54,7 +54,7 @@ exports[`Matches shallow shapshot shapshot 1 1`] = `
06 Nov 2017 - 15:49:35 + 17:49:35
diff --git a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap index 5882241535..e1664e107c 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} From 524a86c5f7b48127704ea0abce645b832679483d Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 19 May 2025 09:32:32 +0300 Subject: [PATCH 04/11] test: fix snapshot timing --- .../__snapshots__/index.jsx.snap | 62 ------------------- .../SubmissionHistoryRow/index.jsx | 51 --------------- 2 files changed, 113 deletions(-) delete mode 100644 __tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/__snapshots__/index.jsx.snap delete mode 100755 __tests__/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx 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 4716dec9ce..0000000000 --- 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 - - 17: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 ed577ffac9..0000000000 --- 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]); - }); -}); -*/ From f1e3d3e80d91c55bc3c37459e83697e81e92d6af Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 19 May 2025 14:38:05 +0530 Subject: [PATCH 05/11] chore(core): general code cleanup remove commented code, add prod copilots url. no issue --- config/production.js | 1 + .../CopilotOpportunityCard/index.jsx | 3 +-- .../Listing/CopilotOpportunityBucket/index.jsx | 13 +------------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/config/production.js b/config/production.js index a0f0d88b86..afebd77d64 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/components/challenge-listing/CopilotOpportunityCard/index.jsx b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx index 7d2f5d4e49..7adff5d7eb 100644 --- a/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx @@ -1,6 +1,5 @@ /** - * Component for rendering a Copilot Opportunity and associated Challenge - * information. Will be contained within a Bucket. + * 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'; diff --git a/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx index f4972648df..09b4c46802 100644 --- a/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/CopilotOpportunityBucket/index.jsx @@ -6,19 +6,16 @@ import { BUCKET_DATA } from 'utils/challenge-listing/buckets'; import SortingSelectBar from 'components/SortingSelectBar'; import Waypoint from 'react-waypoint'; -// import { challenge as challengeUtils } from 'topcoder-react-lib'; import CopilotOpportunityHeader from 'components/challenge-listing/CopilotOpportunityHeader'; import CardPlaceholder from '../../placeholders/ChallengeCard'; -import CopilotOpportunityCard from '../../CopilotOpportunityCard'; // <== Replace with your actual Copilot Card component +import CopilotOpportunityCard from '../../CopilotOpportunityCard'; import './style.scss'; -// const Filter = challengeUtils.filter; const NO_RESULTS_MESSAGE = 'No copilot opportunities found'; const LOADING_MESSAGE = 'Loading Copilot Opportunities'; -// Functional implementation of CopilotOpportunityBucket component export default function CopilotOpportunityBucket({ bucket, challengesUrl, @@ -41,13 +38,6 @@ export default function CopilotOpportunityBucket({ const sortedOpportunities = _.clone(opportunities); sortedOpportunities.sort(Sort[activeSort].func); - // const filteredOpportunities = sortedOpportunities.filter( - // Filter.getReviewOpportunitiesFilterFunction({ - // ...BUCKET_DATA[bucket].filter, - // ...filterState, - // }, challengeTypes), - // ); - const filteredOpportunities = sortedOpportunities; const cards = filteredOpportunities.map(item => ( @@ -142,6 +132,5 @@ CopilotOpportunityBucket.propTypes = { setFilterState: PT.func.isRequired, setSort: PT.func.isRequired, sort: PT.string, - // challengeTypes: PT.arrayOf(PT.shape()).isRequired, setSearchText: PT.func.isRequired, }; From f9800f1f3c853402c3ac92f9ace5c5d8a0e3510d Mon Sep 17 00:00:00 2001 From: Vasilica Date: Tue, 27 May 2025 10:51:24 +0300 Subject: [PATCH 06/11] do not drop the copilot opportunities on bucket change --- src/shared/reducers/challenge-listing/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js index 394375cd55..2719e90409 100644 --- a/src/shared/reducers/challenge-listing/index.js +++ b/src/shared/reducers/challenge-listing/index.js @@ -731,7 +731,7 @@ function create(initialState) { myPastChallenges: [], openForRegistrationChallenges: [], pastChallenges: [], - copilotOpportunities: [], + // copilotOpportunities: [], lastRequestedPageOfActiveChallenges: -1, lastRequestedPageOfOpenForRegistrationChallenges: -1, lastRequestedPageOfMyChallenges: -1, @@ -739,6 +739,7 @@ function create(initialState) { lastRequestedPageOfAllChallenges: -1, lastRequestedPageOfRecommendedChallenges: -1, lastRequestedPageOfPastChallenges: -1, + // lastRequestedPageOfCopilotOpportunities: -1, // lastRequestedPageOfReviewOpportunities: -1, // lastUpdateOfActiveChallenges: 0, loadingActiveChallengesUUID: '', From a212a87bb3cc86125bdf2859714f35ad3ccb8c18 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Tue, 27 May 2025 14:32:16 +0530 Subject: [PATCH 07/11] fix(opportunities): remove search bar and fix typos Removed the unused search bar from the opportunities section and corrected minor typos in labels and headings. Fixes PM-1188 --- .../CopilotOpportunityCard/index.jsx | 2 +- .../challenge-listing/ChallengeSearchBar.jsx | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx index 7adff5d7eb..f240d2d534 100644 --- a/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx @@ -15,7 +15,7 @@ const PROJECT_TYPE_LABELS = { dev: 'Development', ai: 'AI (Artificial Intelligence)', design: 'Design', - datascience: 'DataScience', + datascience: 'Data Science', qa: 'Quality Assurance', }; diff --git a/src/shared/containers/challenge-listing/ChallengeSearchBar.jsx b/src/shared/containers/challenge-listing/ChallengeSearchBar.jsx index b3f3344053..3e8c412c59 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 ); } } From e1955be4d4968e10720dd872b2c6af43c84e2b22 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 17 Jun 2025 10:36:59 +0300 Subject: [PATCH 08/11] PM-967 - expose "TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS" to app --- Dockerfile | 3 +++ build.sh | 1 + config/custom-environment-variables.js | 1 + 3 files changed, 5 insertions(+) diff --git a/Dockerfile b/Dockerfile index 0e7f8c6e2c..113d406006 100644 --- a/Dockerfile +++ b/Dockerfile @@ -154,6 +154,9 @@ ENV GAMIFICATION_ORG_ID=$GAMIFICATION_ORG_ID # Universal nav ENV UNIVERSAL_NAV_URL=$UNIVERSAL_NAV_URL +# Topgear submissions allowed domains +ENV TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS=$TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS + ################################################################################ # Testing and build of the application inside the container. diff --git a/build.sh b/build.sh index a3650db356..9243995e2c 100755 --- a/build.sh +++ b/build.sh @@ -29,6 +29,7 @@ docker build -t $TAG \ --build-arg CONTENTFUL_EDU_CDN_API_KEY=$CONTENTFUL_EDU_CDN_API_KEY \ --build-arg CONTENTFUL_EDU_PREVIEW_API_KEY=$CONTENTFUL_EDU_PREVIEW_API_KEY \ --build-arg FILESTACK_API_KEY=$FILESTACK_API_KEY \ + --build-arg TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS=$TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS \ --build-arg FILESTACK_SUBMISSION_CONTAINER=$FILESTACK_SUBMISSION_CONTAINER \ --build-arg MAILCHIMP_API_KEY=$MAILCHIMP_API_KEY \ --build-arg MAILCHIMP_BASE_URL=$MAILCHIMP_BASE_URL \ diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 38d3c74933..88dd2a9a04 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -117,4 +117,5 @@ module.exports = { ORG_ID: 'GAMIFICATION_ORG_ID', ENABLE_BADGE_UI: 'GAMIFICATION_ENABLE_BADGE_UI', }, + TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS: 'TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS', }; From 42a74269be200c347497d0fedb4e1478ba6e990f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 17 Jun 2025 16:27:07 +0300 Subject: [PATCH 09/11] PM-967 - dockerfile: add TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS arg --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 113d406006..d1c8889411 100644 --- a/Dockerfile +++ b/Dockerfile @@ -86,6 +86,9 @@ ARG GAMIFICATION_ORG_ID # Universal Nav ARG UNIVERSAL_NAV_URL +# Topgear submissions allowed domains +ARG TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS + ################################################################################ # Setting of environment variables in the Docker image. From 5e042201c1845d1140fe69ecf88f6cd0641c7009 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 17 Jun 2025 19:01:22 +0300 Subject: [PATCH 10/11] PM-967 - keep env var as string --- config/default.js | 2 +- .../components/SubmissionPage/FilestackFilePicker/index.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default.js b/config/default.js index 1b8eeecdf8..8f8f4d11a8 100644 --- a/config/default.js +++ b/config/default.js @@ -473,5 +473,5 @@ module.exports = { ACCOUNT_SETTINGS_REDIRECT_URL: 'https://account-settings.topcoder-dev.com', INNOVATION_CHALLENGES_TAG: 'Innovation Challenge', PLATFORM_SITE_URL: 'https://platform.topcoder-dev.com', - TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS: ['wipro365.sharepoint.com', 'wipro365-my.sharepoint.com', 'wipro365-my.sharepoint.com.mcas.ms'], + TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS: 'wipro365.sharepoint.com|wipro365-my.sharepoint.com|wipro365-my.sharepoint.com.mcas.ms', }; diff --git a/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx b/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx index 8864968f22..9d828f6371 100644 --- a/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx +++ b/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx @@ -136,7 +136,7 @@ class FilestackFilePicker extends React.Component { } isDomainAllowed(url) { - const domainReg = new RegExp(`^https?://(${config.TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS.join('|')})/.+`); + const domainReg = new RegExp(`^https?://(${config.TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS})/.+`); return !!url.match(domainReg); } From 5f7b109880bb758f5e184b43148a5664fca35f36 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 19 Jun 2025 14:17:18 +0530 Subject: [PATCH 11/11] fix: canceled status color --- .../challenge-listing/CopilotOpportunityCard/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx index f240d2d534..279ceede03 100644 --- a/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/CopilotOpportunityCard/index.jsx @@ -58,7 +58,7 @@ function CopilotOpportunityCard({
{PROJECT_TYPE_LABELS[opportunity.type]}
-
+
{opportunity.status}