Skip to content

[PROD RELEASE] - Copilot Portal #1117

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 15 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/apps/copilots/src/models/CopilotOpportunity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export interface CopilotOpportunity {
startDate: Date,
tzRestrictions: 'yes' | 'no',
createdAt: Date,
members: Array<number>,
canApplyAsCopilot: boolean,

Choose a reason for hiding this comment

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

The type of the members property has been changed to canApplyAsCopilot, which is a boolean. Ensure that this change is intentional and that all references to members in the codebase are updated accordingly to prevent any runtime errors.

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ const ApplyOpportunityModal: FC<ApplyOpportunityModalProps> = props => {
size='lg'
title={
success
? `Your Application for ${props.projectName} Has Been Received!`
: `Confirm Your Copilot Application for ${props.projectName}`
? 'Your Application Has Been Received!'
: 'Confirm Your Copilot Application'
}
buttons={
!success ? (
Expand All @@ -62,21 +62,21 @@ const ApplyOpportunityModal: FC<ApplyOpportunityModalProps> = props => {
<div className={styles.info}>
{
success
? `We appreciate the time and effort you've taken to apply
for this exciting opportunity. Our team is committed
to providing a seamless and efficient process to ensure a
great experience for all copilots. We will review your application
within short time.`
? `Thank you for taking the time to apply for this exciting opportunity.
We truly value your interest and effort.
Your application will be reviewed promptly.`
: `We're excited to see your interest in joining our team as a copilot
for the ${props.projectName} project! Before we proceed, we want to
for the "${props.projectName}" project! Before we proceed, we want to

Choose a reason for hiding this comment

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

Consider using consistent quotation marks for the project name. Either use single quotes or double quotes throughout the codebase for consistency.

ensure that you have carefully reviewed the project requirements and
are committed to meeting them.`
are committed to meeting them. Please write below the reason(s)
why you believe you're a good fit for this project
(e.g., previous experience, availability, etc.).`
}
</div>
{
!success && (
<InputTextarea
name='Notes'
name='Reason'

Choose a reason for hiding this comment

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

The name attribute has been changed from 'Notes' to 'Reason'. Ensure that this change is reflected wherever this component is used or referenced to avoid potential issues.

onChange={onChange}
value={notes}
error={error}
Expand Down
207 changes: 125 additions & 82 deletions src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { mutate } from 'swr'
import { toast } from 'react-toastify'

Choose a reason for hiding this comment

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

Consider verifying if toast is used in the component. If not, the import might be unnecessary and could be removed to keep the code clean.

import moment from 'moment'

import {
Expand All @@ -28,13 +29,15 @@ import { textFormatDateLocaleShortString } from '~/libs/shared'

import { CopilotApplication } from '../../models/CopilotApplication'
import {
cancelCopilotOpportunity,

Choose a reason for hiding this comment

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

The cancelCopilotOpportunity import is added but not used in the current code. Consider removing it if it's unnecessary to avoid unused imports.

copilotBaseUrl,
CopilotOpportunityResponse,
useCopilotApplications,
useCopilotOpportunity,
} from '../../services/copilot-opportunities'
import { FormattedMembers, useMembers } from '../../services/members'
import { copilotRoutesMap } from '../../copilots.routes'
import { ProjectType } from '../../constants'

import { ApplyOpportunityModal } from './apply-opportunity-modal'
import {
Expand Down Expand Up @@ -103,7 +106,7 @@ const CopilotOpportunityDetails: FC<{}> = () => {

const onApplied: () => void = useCallback(() => {
mutate(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}/applications`)
mutate(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}`)
mutate(`${copilotBaseUrl}/copilot/opportunity/${opportunityId}`)

Choose a reason for hiding this comment

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

The URL path has been changed from /copilots/opportunity/ to /copilot/opportunity/. Ensure that this change is intentional and that the new endpoint is correct and available, as this could affect the data fetching logic.

}, [])

const onCloseApplyModal: () => void = useCallback(() => {
Expand All @@ -119,13 +122,42 @@ const CopilotOpportunityDetails: FC<{}> = () => {
)
}

async function cancelCopilotOpportunityHandler(): Promise<void> {
if (opportunityId) {
await cancelCopilotOpportunity(opportunityId)
mutate(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}/applications`)
mutate(`${copilotBaseUrl}/copilot/opportunity/${opportunityId}`)
toast.success('Canceled copilot opportunity successfully')
}

}

const applyCopilotOpportunityButton: ButtonProps = {
label: 'Apply as Copilot',
onClick: () => setShowApplyOpportunityModal(true),
}

const cancelCopilotOpportunityButton: ButtonProps = {
label: 'Cancel opportunity',
onClick: cancelCopilotOpportunityHandler,
}

const application = copilotApplications && copilotApplications[0]
const isAlreadyMemberOfTheProject = profile && opportunity?.members?.includes(profile.userId)

const getOpportunityType = (type: string): ProjectType => {
switch (type) {
case 'ai':
return ProjectType.ai
case 'datascience':
return ProjectType.datascience
case 'dev':
return ProjectType.developement

Choose a reason for hiding this comment

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

Typo in 'developement'. It should be 'development'.

case 'design':
return ProjectType.design
default:
return ProjectType.qa
}
}

return (
<ContentLayout
Expand All @@ -135,7 +167,11 @@ const CopilotOpportunityDetails: FC<{}> = () => {
&& copilotApplications
&& copilotApplications.length === 0
&& opportunity?.status === 'active'
&& !isAlreadyMemberOfTheProject ? applyCopilotOpportunityButton : undefined
&& opportunity?.canApplyAsCopilot ? applyCopilotOpportunityButton : undefined

Choose a reason for hiding this comment

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

Consider adding a null check for opportunity before accessing canApplyAsCopilot to prevent potential runtime errors if opportunity is null or undefined.

}
secondaryButtonConfig={
opportunity?.status === 'active'

Choose a reason for hiding this comment

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

Consider adding a null check for opportunity before accessing status to prevent potential runtime errors if opportunity is null or undefined.

&& isAdminOrPM ? cancelCopilotOpportunityButton : undefined
}
infoComponent={(isCopilot && !(copilotApplications
&& copilotApplications.length === 0
Expand All @@ -156,94 +192,101 @@ const CopilotOpportunityDetails: FC<{}> = () => {
{isValidating && !showNotFound && (
<LoadingSpinner />
) }
<h1 className={styles.header}>
{opportunity?.projectName}
</h1>
<div className={styles.infoRow}>
<div className={styles.infoColumn}>
<IconOutline.ClipboardCheckIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Status</span>
<span className={styles.infoValue}>{opportunity?.status}</span>
<div className={styles.wrapper}>
<h1 className={styles.header}>
{opportunity?.projectName}
</h1>
<div className={styles.infoRow}>
<div className={styles.infoColumn}>
<IconOutline.ClipboardCheckIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Status</span>
<span className={styles.infoValue}>{opportunity?.status}</span>
</div>
</div>
</div>
<div className={styles.infoColumn}>
<IconOutline.PlayIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Start Date</span>
<span className={styles.infoValue}>
{moment(opportunity?.startDate)
.format('MMM D, YYYY')}

</span>
<div className={styles.infoColumn}>
<IconOutline.PlayIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Start Date</span>
<span className={styles.infoValue}>
{moment(opportunity?.startDate)
.format('MMM D, YYYY')}

</span>
</div>
</div>
</div>
<div className={styles.infoColumn}>
<IconOutline.CalendarIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Duration</span>
<span className={styles.infoValue}>
{opportunity?.numWeeks}
{' '}
weeks
</span>
<div className={styles.infoColumn}>
<IconOutline.CalendarIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Duration</span>
<span className={styles.infoValue}>
{opportunity?.numWeeks}
{' '}
weeks
</span>
</div>
</div>
</div>
<div className={styles.infoColumn}>
<IconOutline.ClockIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Hours</span>
<span className={styles.infoValue}>
{opportunity?.numHoursPerWeek}
{' '}
hours/week
</span>
<div className={styles.infoColumn}>
<IconOutline.ClockIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Hours</span>
<span className={styles.infoValue}>
{opportunity?.numHoursPerWeek}
{' '}
hours/week
</span>
</div>
</div>
</div>
<div className={styles.infoColumn}>
<IconOutline.CogIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Type</span>
<span className={styles.infoValue}>{opportunity?.type}</span>
<div className={styles.infoColumn}>
<IconOutline.CogIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Type</span>
<span
className={styles.infoValue}
>
{opportunity?.type && getOpportunityType(opportunity?.type)}

Choose a reason for hiding this comment

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

Consider using a consistent approach for rendering the opportunity?.type. The function getOpportunityType is used here, but it's not clear if this is necessary or if it should be applied elsewhere. Ensure consistency across similar data rendering.

</span>
</div>
</div>
</div>
<div className={styles.infoColumn}>
<IconOutline.GlobeAltIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Working Hours</span>
<span className={styles.infoValue}>{opportunity?.tzRestrictions}</span>
<div className={styles.infoColumn}>
<IconOutline.GlobeAltIcon className={styles.icon} />
<div className={styles.infoText}>
<span className={styles.infoHeading}>Working Hours</span>
<span className={styles.infoValue}>{opportunity?.tzRestrictions}</span>
</div>
</div>
</div>
</div>
{
initialized && (
<TabsNavbar
defaultActive={activeTab}
onChange={handleTabChange}
tabs={getCopilotDetailsTabsConfig(isAdminOrPM)}
{
initialized && (
<TabsNavbar
defaultActive={activeTab}
onChange={handleTabChange}
tabs={getCopilotDetailsTabsConfig(isAdminOrPM)}
/>
)
}
{activeTab === CopilotDetailsTabViews.details && <OpportunityDetails opportunity={opportunity} />}
{activeTab === CopilotDetailsTabViews.applications && isAdminOrPM && opportunity && (
<CopilotApplications
copilotApplications={copilotApplications}
opportunity={opportunity}
members={members}
/>
)
}
{activeTab === CopilotDetailsTabViews.details && <OpportunityDetails opportunity={opportunity} />}
{activeTab === CopilotDetailsTabViews.applications && isAdminOrPM && opportunity && (
<CopilotApplications
copilotApplications={copilotApplications}
opportunity={opportunity}
members={members}
/>
)}
)}

{
showApplyOpportunityModal
&& opportunity && (
<ApplyOpportunityModal
copilotOpportunityId={opportunity?.id}
onClose={onCloseApplyModal}
projectName={opportunity?.projectName}
onApplied={onApplied}
/>
)
}
</div>

{
showApplyOpportunityModal
&& opportunity && (
<ApplyOpportunityModal
copilotOpportunityId={opportunity?.id}
onClose={onCloseApplyModal}
projectName={opportunity?.projectName}
onApplied={onApplied}
/>
)
}
</ContentLayout>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@import '@libs/ui/styles/includes';

.wrapper {
min-height: 800px;
}

.header {
display: flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const CopilotApplicationAction = (
): JSX.Element => {
const { opportunityId }: {opportunityId?: string} = useParams<{ opportunityId?: string }>()
const isInvited = useMemo(
() => allCopilotApplications.findIndex(item => item.status === CopilotApplicationStatus.INVITED) > -1,
() => allCopilotApplications

Choose a reason for hiding this comment

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

Consider using optional chaining (?.) to safely access allCopilotApplications instead of using a logical AND (&&). This can make the code more concise and readable: allCopilotApplications?.findIndex(item => item.status === CopilotApplicationStatus.INVITED) > -1.

&& allCopilotApplications.findIndex(item => item.status === CopilotApplicationStatus.INVITED) > -1,
[allCopilotApplications],
)
const onClick = useCallback(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@ const OpportunityDetails: FC<{
<div>
<h2 className={styles.subHeading}> Required skills </h2>
<div className={styles.skillsContainer}>
{props.opportunity?.skills.map((skill: any) => (
<div key={skill.id} className={styles.skillPill}>
{skill.name}
</div>
))}
{props.opportunity?.skills.map(item => item.name)

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 variable name instead of item for better readability, such as skill.

.join(',')}

Choose a reason for hiding this comment

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

The current implementation joins skill names into a single string separated by commas. If the intention is to display skills as a list, consider using a list or similar structure to maintain the previous visual separation.

</div>
<h2 className={styles.subHeading}> Description </h2>
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ const tableColumns: TableColumn<CopilotOpportunity>[] = [
propertyName: 'startDate',
type: 'date',
},
{
label: 'Duration(Weeks)',
propertyName: 'numWeeks',
type: 'text',
},
{
label: 'Complexity',
propertyName: 'complexity',
Expand Down
Loading