Skip to content

PM-1188 copilot opportunities on challenge feed #7094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 19, 2025
Merged

PM-1188 copilot opportunities on challenge feed #7094

merged 5 commits into from
May 19, 2025

Conversation

himaniraghav3
Copy link
Collaborator

https://topcoder.atlassian.net/browse/PM-1188

Add support for fetching and displaying copilot opportunities within the challenge feed, including dynamic sorting and pagination handling.

This excludes search over opportunities. We have added sorting over title, project track, and status.
Deafult sort prioritises active oportunities and sorts them on created Date.

add support for fetching and displaying copilot opportunities within the challenge feed, including dynamic sorting and pagination handling.

refs pm-1188
* @return {Promise<{uuid: string, loaded: object}>} Action result
*/
function getCopilotOpportunitiesDone(uuid, page) {
return getCopilotOpportunities(page, COPILOT_OPPORTUNITY_PAGE_SIZE)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider handling the case where getCopilotOpportunities might return a non-object or unexpected data structure. This could prevent potential runtime errors if the API response changes.

return getCopilotOpportunities(page, COPILOT_OPPORTUNITY_PAGE_SIZE)
.then(loaded => ({ uuid, loaded }))
.catch((error) => {
fireErrorMessage('Error Getting Copilot Opportunities', error.content || error);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fireErrorMessage function is called with error.content || error. Ensure that error.content is a valid property and consider logging the entire error object for better debugging information.

import './style.scss';

const PROJECT_TYPE_LABELS = {
dev: 'Development',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a default label for project types that are not listed in PROJECT_TYPE_LABELS to handle unexpected values gracefully.


<div styleName="right-panel">
<div styleName="type">
<span>{PROJECT_TYPE_LABELS[opportunity.type]}</span>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a fallback for PROJECT_TYPE_LABELS[opportunity.type] in case opportunity.type is not a key in PROJECT_TYPE_LABELS to prevent rendering undefined.

}

CopilotOpportunityCard.propTypes = {
opportunity: PT.shape().isRequired,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opportunity prop type should be more specific. Define the shape with expected properties and their types to improve type safety and documentation.

font-size: 12px;
color: $tc-gray-60;
margin-right: $challenge-space-10;
line-height: ($challenge-space-10) + 2;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line-height calculation ($challenge-space-10) + 2 might not work as intended. Consider using calc() for arithmetic operations in CSS, like calc(#{$challenge-space-10} + 2px).

color: green;
line-height: $challenge-space-20;
margin-right: $challenge-space-20;
min-width: $challenge-space-50 + 2;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The min-width property value $challenge-space-50 + 2 might not be calculated correctly. Consider using calc() for arithmetic operations, like calc(#{$challenge-space-50} + 2px).


function CopilotOpportunityHeader() {
return (
<div styleName="copilotOpportunityHeader">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a more descriptive class name for copilotOpportunityHeader to avoid potential conflicts with other styles and to improve readability.

function CopilotOpportunityHeader() {
return (
<div styleName="copilotOpportunityHeader">
<div styleName="left-panel">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The left-panel class name could be more descriptive to better convey its purpose or content.

</div>
</div>

<div styleName="right-panel">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a more descriptive class name for right-panel to improve clarity and maintainability.

</div>

<div styleName="right-panel">
<div styleName="type">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type class name is quite generic. Consider using a more specific name to avoid potential conflicts and improve readability.

<div styleName="type">
<span>Type</span>
</div>
<div styleName="status">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status class name is very generic. Consider using a more descriptive name to avoid potential conflicts and improve maintainability.

<div styleName="status">
<span>Status</span>
</div>
<div styleName="numHours">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a more descriptive class name for numHours to better convey its purpose and improve code readability.

@@ -0,0 +1,93 @@
@import '~styles/mixins';

$challenge-space-20: $base-unit * 4;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming $challenge-space-20 to something more descriptive, such as $challenge-padding-small, to better convey its purpose.

@import '~styles/mixins';

$challenge-space-20: $base-unit * 4;
$challenge-space-30: $base-unit * 6;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming $challenge-space-30 to something more descriptive, such as $challenge-padding-large, to better convey its purpose.

.copilotOpportunityHeader {
@include roboto-medium;

display: flex;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The justify-content: flex-start; property is redundant here since flex-start is the default value for justify-content. Consider removing it for cleaner code.

padding: $challenge-space-20 0;
color: $tc-gray-60;
font-size: 12px;
margin-left: 24px;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The margin-left: 24px; might cause alignment issues on smaller screens. Ensure this is the intended behavior or consider using a responsive unit.

.left-panel {
display: flex;
justify-content: flex-start;
width: 45.5%;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The width of 45.5% for .left-panel might lead to layout issues when combined with the 50% width of .right-panel. Ensure this is the intended design or adjust the widths to fit within 100%.

.challenge-details {
display: inline-block;
vertical-align: baseline;
width: 82%;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The width of 82% for .challenge-details might cause layout issues when combined with the margin-right. Ensure this is the intended design or adjust the width to fit within the container.

@@ -216,6 +217,7 @@ export default function FiltersPanel({
/>
</div>
</div>
{ !isCopilotOpportunitiesBucket && (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the closing parenthesis for the conditional rendering is missing. Ensure that the JSX block is properly closed with a parenthesis to avoid syntax errors.

@@ -257,8 +259,9 @@ export default function FiltersPanel({
</div>
</div>
</div>
)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The closing parenthesis for the conditional rendering block seems to be misplaced. Ensure that the conditional logic is correctly structured to avoid rendering issues.

@@ -456,7 +459,7 @@ export default function FiltersPanel({
)
}
</div>

{ !isCopilotOpportunitiesBucket && (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition !isCopilotOpportunitiesBucket is added here to wrap the buttons div. Ensure that this condition is necessary and correctly implemented to avoid hiding buttons unintentionally when isCopilotOpportunitiesBucket is true.

// 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment // <== Replace with your actual Copilot Card component suggests that CopilotOpportunityCard might be a placeholder or temporary component. Ensure that this is the intended component for production use.

sort,
setSearchText,
}) {
if (!opportunities.length && !loadMore) return null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition if (!opportunities.length && !loadMore) return null; might cause the component to return null even when loading is true. Consider revising this logic if you want to show a loading state when there are no opportunities but data is still being fetched.

{cards}
{!loading && filteredOpportunities.length === 0 && (
<div>
<div styleName="copilot-opportunity-bucket">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested SortingSelectBar within the !loading && filteredOpportunities.length === 0 condition seems redundant since a similar component is rendered above. Consider removing this duplication unless there is a specific reason to render it twice.

CopilotOpportunityBucket.propTypes = {
bucket: PT.string.isRequired,
challengesUrl: PT.string.isRequired,
expandedTags: PT.arrayOf(PT.number),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expandedTags prop is defined as PT.arrayOf(PT.number), but it defaults to an empty array. Ensure that this type definition aligns with the expected data structure, especially if expandedTags might contain non-numeric values.

expandedTags: PT.arrayOf(PT.number),
expandTag: PT.func,
// filterState: PT.shape().isRequired,
opportunities: PT.arrayOf(PT.shape()).isRequired,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opportunities prop is defined as PT.arrayOf(PT.shape()).isRequired, which lacks specific shape details. Consider defining the expected shape of the objects within the opportunities array for better type checking and documentation.

margin-left: 0;
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a good practice to ensure that files end with a newline character. This can prevent issues with certain tools and improve readability when concatenating files.

setSearchText={setSearchText}
/>
);
} else if (isCopilotOpportunitiesBucket(bucket)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function isCopilotOpportunitiesBucket is used here, but it's not clear from the diff whether this function is defined elsewhere or imported. Ensure that this function is properly defined or imported to avoid runtime errors.

needLoad={needLoad}
loading={loadingCopilotOpportunities}
loadMore={loadMoreCopilotOpportunities}
opportunities={copilotOpportunities}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable loadMoreCopilotOpportunities is used here, but it's not clear from the diff whether this variable is defined elsewhere or imported. Ensure that this variable is properly defined or imported to avoid runtime errors.

needLoad={needLoad}
loading={loadingCopilotOpportunities}
loadMore={loadMoreCopilotOpportunities}
opportunities={copilotOpportunities}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming loadMoreCopilotOpportunities to loadMoreOpportunities for consistency with the naming convention used for loadMoreReviewOpportunities.

filterState={filterState}
keepPlaceholders={keepPastPlaceholders}
needLoad={needLoad}
loading={loadingCopilotOpportunities}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable loadingCopilotOpportunities is used here, but it's not clear from the diff whether this variable is defined elsewhere or imported. Ensure that this variable is properly defined or imported to avoid runtime errors.

needLoad={needLoad}
loading={loadingCopilotOpportunities}
loadMore={loadMoreCopilotOpportunities}
opportunities={copilotOpportunities}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable copilotOpportunities is used here, but it's not clear from the diff whether this variable is defined elsewhere or imported. Ensure that this variable is properly defined or imported to avoid runtime errors.

@@ -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()),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider specifying the shape of the objects within the copilotOpportunities array for better type checking and clarity. This will help ensure that all necessary properties are present and correctly typed.

@@ -377,6 +407,7 @@ const mapStateToProps = (state) => {
allChallengesLoaded: cl.allChallengesLoaded,
allPastChallengesLoaded: cl.allPastChallengesLoaded,
allOpenForRegistrationChallengesLoaded: cl.allOpenForRegistrationChallengesLoaded,
// allCopilotOpportunitiesLoaded: cl.allCopilotOpportunitiesLoaded,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line for allCopilotOpportunitiesLoaded is commented out. If this is intentional, consider removing it if it's not needed, or uncommenting it if it should be part of the functionality.

@@ -601,6 +606,13 @@ export class ListingContainer extends React.Component {
);
}

let loadMoreCopilotOpportunities;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming loadMoreCopilotOpportunities to handleLoadMoreCopilotOpportunities to better convey that this is a function handling an action.

@@ -601,6 +606,13 @@ export class ListingContainer extends React.Component {
);
}

let loadMoreCopilotOpportunities;
if (!allCopilotOpportunitiesLoaded) {
loadMoreCopilotOpportunities = () => getCopilotOpportunities(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear what the argument 1 + lastRequestedPageOfCopilotOpportunities represents. Ensure that the logic for incrementing the page number is correct and consider using a named constant or variable for clarity.

@@ -768,6 +784,7 @@ ListingContainer.propTypes = {
loadingUuid: PT.string.isRequired,
timestamp: PT.number.isRequired,
}).isRequired,
copilotOpportunities: PT.arrayOf(PT.shape()).isRequired,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PT.shape() used for copilotOpportunities is currently empty. Consider defining the expected shape of the objects within the copilotOpportunities array to ensure proper type checking and validation.

@@ -872,8 +894,10 @@ const mapStateToProps = (state, ownProps) => {
lastRequestedPageOfAllChallenges: cl.lastRequestedPageOfAllChallenges,
lastRequestedPageOfPastChallenges: cl.lastRequestedPageOfPastChallenges,
lastRequestedPageOfReviewOpportunities: cl.lastRequestedPageOfReviewOpportunities,
lastRequestedPageOfCopilotOpportunities: cl.lastRequestedPageOfCopilotOpportunities,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider verifying if lastRequestedPageOfCopilotOpportunities is correctly initialized and used throughout the application to prevent potential undefined behavior.

// lastUpdateOfActiveChallenges: cl.lastUpdateOfActiveChallenges,
loadingActiveChallengesUUID: cl.loadingActiveChallengesUUID,
loadingCopilotOpportunitiesUUID: cl.loadingCopilotOpportunitiesUUID,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that loadingCopilotOpportunitiesUUID is properly managed and updated in the application state to avoid any inconsistencies or errors during data fetching.

@@ -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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider checking if cl.loadingCopilotOpportunitiesUUID needs to be wrapped with Boolean() for consistency with the other loading flags.

getCopilotOpportunities: (page) => {
const uuid = shortId();
dispatch(a.getCopilotOpportunitiesInit(uuid, page));
dispatch(a.getCopilotOpportunitiesDone(uuid, page));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the getCopilotOpportunitiesDone function is being dispatched without a token argument, unlike getReviewOpportunitiesDone which includes it. If authentication is required for fetching copilot opportunities, consider adding the token parameter.

* @return {Object} New state
*/
function onGetCopilotOpportunitiesDone(state, { payload, error }) {
if (error) return state;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider handling the error case more explicitly rather than returning the current state. This could involve logging the error or updating the state to reflect the error condition.

if (uuid !== state.loadingCopilotOpportunitiesUUID) return state;

const ids = new Set();
loaded.forEach(item => ids.add(item.id));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a Set to track IDs is efficient for ensuring uniqueness. However, consider adding a comment explaining why this approach is chosen, especially if the order of items is important.

const ids = new Set();
loaded.forEach(item => ids.add(item.id));
const copilotOpportunities = state.copilotOpportunities
.filter(item => !ids.has(item.id))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter and concat operations could be optimized by using a single reduce operation to build the new array, which might improve performance slightly if the arrays are large.

* @returns {Promise<Object>} 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)}`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding error handling for the fetch request to manage potential network errors or unsuccessful responses. This will improve the robustness of the function.

apply consistent formatting and fix linting errors in all scss files to maintain code quality.

no issue
@kkartunov kkartunov requested a review from jmgasper May 15, 2025 15:46
@@ -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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is hard-coded value we need to add it also in https://github.com/topcoder-platform/community-app/blob/develop/config/production.js

const sortedOpportunities = _.clone(opportunities);
sortedOpportunities.sort(Sort[activeSort].func);

// const filteredOpportunities = sortedOpportunities.filter(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?
Let's remove commented out code for going upstream with this.

kkartunov and others added 2 commits May 19, 2025 09:32
remove commented code, add prod copilots url.

no issue
@kkartunov kkartunov self-requested a review May 19, 2025 10:45
Copy link
Collaborator

@kkartunov kkartunov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Merging this.

@kkartunov kkartunov merged commit 03bcfff into develop May 19, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants