diff --git a/src/apps/copilots/src/copilots.routes.tsx b/src/apps/copilots/src/copilots.routes.tsx index b0d3ca78e..d06bd80ba 100644 --- a/src/apps/copilots/src/copilots.routes.tsx +++ b/src/apps/copilots/src/copilots.routes.tsx @@ -5,6 +5,9 @@ const CopilotsApp: LazyLoadedComponent = lazyLoad(() => import('./CopilotsApp')) const CopilotOpportunityList: LazyLoadedComponent = lazyLoad(() => import('./pages/copilot-opportunity-list/index')) const CopilotsRequests: LazyLoadedComponent = lazyLoad(() => import('./pages/copilot-requests/index')) const CopilotsRequestForm: LazyLoadedComponent = lazyLoad(() => import('./pages/copilot-request-form/index')) +const CopilotOpportunityDetails: LazyLoadedComponent = lazyLoad( + () => import('./pages/copilot-opportunity-details/index'), +) export const rootRoute: string = ( EnvironmentConfig.SUBDOMAIN === AppSubdomain.copilots ? '' : `/${AppSubdomain.copilots}` @@ -34,6 +37,11 @@ export const childRoutes = [ id: 'CopilotRequestDetails', route: '/requests/:requestId', }, + { + element: , + id: 'CopilotOpportunityDetails', + route: '/opportunity/:opportunityId', + }, ] as const type RouteMap = { diff --git a/src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx b/src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx new file mode 100644 index 000000000..4caffd9d5 --- /dev/null +++ b/src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx @@ -0,0 +1,127 @@ +import { FC, useEffect, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' + +import { + ContentLayout, + IconOutline, + LoadingSpinner, + PageTitle, +} from '~/libs/ui' + +import { CopilotOpportunityResponse, useCopilotOpportunity } from '../../services/copilot-opportunities' +import { copilotRoutesMap } from '../../copilots.routes' + +import styles from './styles.module.scss' + +const CopilotOpportunityDetails: FC<{}> = () => { + const { opportunityId }: {opportunityId?: string} = useParams<{ opportunityId?: string }>() + const navigate = useNavigate() + const [showNotFound, setShowNotFound] = useState(false) + + if (!opportunityId) { + navigate(copilotRoutesMap.CopilotOpportunityList) + } + + const { data: opportunity, isValidating }: CopilotOpportunityResponse = useCopilotOpportunity(opportunityId) + + useEffect(() => { + const timer = setTimeout(() => { + if (!opportunity) { + setShowNotFound(true) + } + }, 2000) + + return () => clearTimeout(timer) // Cleanup on unmount + }, [opportunity]) + + if (!opportunity && showNotFound) { + return ( + + Opportunity Not Found +

The requested opportunity does not exist.

+
+ ) + } + + return ( + + Copilot Opportunity + {isValidating && !showNotFound && ( + + ) } +

+ {opportunity?.projectName} +

+
+
+ +
+ Status + {opportunity?.status} +
+
+
+ +
+ Duration + + {opportunity?.numWeeks} + {' '} + weeks + +
+
+
+ +
+ Hours + + {opportunity?.numHoursPerWeek} + {' '} + hours/week + +
+
+
+ +
+ Type + {opportunity?.type} +
+
+
+ +
+ Working Hours + {opportunity?.tzRestrictions} +
+
+
+
+
+

Required skills

+
+ {opportunity?.skills.map((skill: any) => ( +
+ {skill.name} +
+ ))} +
+

Description

+

+ {opportunity?.overview} +

+
+
+

Complexity

+ {opportunity?.complexity} + +

Requires Communication

+ {opportunity?.requiresCommunication} +
+
+
+ ) +} + +export default CopilotOpportunityDetails diff --git a/src/apps/copilots/src/pages/copilot-opportunity-details/styles.module.scss b/src/apps/copilots/src/pages/copilot-opportunity-details/styles.module.scss new file mode 100644 index 000000000..a51bd31bc --- /dev/null +++ b/src/apps/copilots/src/pages/copilot-opportunity-details/styles.module.scss @@ -0,0 +1,93 @@ +@import '@libs/ui/styles/includes'; + +.header { + display: flex; + align-items: center; + justify-content: center; + text-transform: uppercase; + font-family: $font-barlow-condensed; + font-size: 50px; + font-weight: $font-weight-medium; + margin-top: $sp-2; + padding: $sp-6 0; + color: $teal-100; +} + +.subHeading { + margin-top: $sp-8; +} + +.content { + margin-top: $sp-6; + display: flex; + flex-direction: row; + gap: 100px; +} + +.content > div:first-child { + flex: 3; +} + +.content > div:last-child { + flex: 1; +} + +.skillsContainer { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: $sp-2; +} + +.skillPill { + background-color: $teal-100; + color: white; + padding: 4px 8px; + border-radius: 10px; + white-space: nowrap; + font-size: 14px; +} + +.infoRow { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + margin: 16px 80px; + padding: 12px 0; +} + +.infoText { + display: flex; + flex-direction: column; +} + +.infoHeading { + font-size: 14px; + color: #666; +} + +.infoValue { + font-size: 14px; + font-weight: 600; + text-transform: capitalize; + color: #333; +} + +.infoColumn { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 120px; +} + +.icon { + width: 30px; + height: 30px; + color: $teal-100; +} + +.textCaps { + text-transform: capitalize; +} \ No newline at end of file diff --git a/src/apps/copilots/src/pages/copilot-opportunity-list/index.tsx b/src/apps/copilots/src/pages/copilot-opportunity-list/index.tsx index c31b4a5be..927073d6d 100644 --- a/src/apps/copilots/src/pages/copilot-opportunity-list/index.tsx +++ b/src/apps/copilots/src/pages/copilot-opportunity-list/index.tsx @@ -1,4 +1,5 @@ import { FC, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' import classNames from 'classnames' import { @@ -10,6 +11,7 @@ import { } from '~/libs/ui' import { CopilotOpportunity } from '../../models/CopilotOpportunity' +import { copilotRoutesMap } from '../../copilots.routes' import { CopilotOpportunitiesResponse, useCopilotOpportunities } from '../../services/copilot-opportunities' import styles from './styles.module.scss' @@ -72,6 +74,7 @@ const tableColumns: TableColumn[] = [ ] const CopilotOpportunityList: FC<{}> = () => { + const navigate = useNavigate() const { data: opportunities, isValidating, size, setSize, @@ -83,6 +86,10 @@ const CopilotOpportunityList: FC<{}> = () => { setSize(size + 1) } + function handleRowClick(opportunity: CopilotOpportunity): void { + navigate(copilotRoutesMap.CopilotOpportunityDetails.replace(':opportunityId', `${opportunity.id}`)) + } + const opportunitiesLoading = isValidating return ( @@ -95,6 +102,7 @@ const CopilotOpportunityList: FC<{}> = () => { data={tableData} moreToLoad={isValidating || opportunities.length > 0} onLoadMoreClick={loadMore} + onRowClick={handleRowClick} /> {opportunitiesLoading && ( diff --git a/src/apps/copilots/src/services/copilot-opportunities.ts b/src/apps/copilots/src/services/copilot-opportunities.ts index 50b01ceed..718f0fb92 100644 --- a/src/apps/copilots/src/services/copilot-opportunities.ts +++ b/src/apps/copilots/src/services/copilot-opportunities.ts @@ -1,7 +1,9 @@ +import useSWR, { SWRResponse } from 'swr' import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite' import { EnvironmentConfig } from '~/config' import { xhrGetAsync } from '~/libs/core' +import { buildUrl } from '~/libs/shared/lib/utils/url' import { CopilotOpportunity } from '../models/CopilotOpportunity' @@ -59,3 +61,23 @@ export const useCopilotOpportunities = (): CopilotOpportunitiesResponse => { return { data: opportunities, isValidating, setSize: (s: number) => { setSize(s) }, size } } + +export type CopilotOpportunityResponse = SWRResponse + +/** + * Custom hook to fetch copilot opportunity by id. + * + * @param {string} opportunityId - The unique identifier of the copilot request. + * @returns {CopilotOpportunityResponse} - The response containing the copilot request data. + */ +export const useCopilotOpportunity = (opportunityId?: string): CopilotOpportunityResponse => { + const url = opportunityId ? buildUrl(`${baseUrl}/copilots/opportunities/${opportunityId}`) : undefined + + const fetcher = (urlp: string): Promise => xhrGetAsync(urlp) + .then(copilotOpportunityFactory) + + return useSWR(url, fetcher, { + refreshInterval: 0, + revalidateOnFocus: false, + }) +}