Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1f9388d
PM-5255 - showcase in customer portal
vas3a Jul 7, 2026
58bdc0f
PM-5255 - showcase in customer portal
vas3a Jul 8, 2026
e7f8c14
PM-5255 - showcase customer portal search
vas3a Jul 8, 2026
90d4b40
lint
vas3a Jul 9, 2026
aae3e78
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into P…
vas3a Jul 9, 2026
91906ed
Remove unused comp
vas3a Jul 9, 2026
8c46f80
Fix for PM-5592
jmgasper Jul 10, 2026
2364d0a
PM-5585: Update Flexi Talent bucket note
jmgasper Jul 10, 2026
72fdf7c
PM-5584: Hide time left for inactive assignments
jmgasper Jul 10, 2026
a706b95
Merge pull request #2031 from topcoder-platform/PM-5584
jmgasper Jul 10, 2026
2b3a6cc
Merge pull request #2030 from topcoder-platform/PM-5585
jmgasper Jul 10, 2026
80b123a
Merge pull request #2029 from topcoder-platform/PM-5255_showcase-cust…
vas3a Jul 10, 2026
00d9c71
PM-5255 - shwocase in customer portal - post
vas3a Jul 10, 2026
698a338
lint
vas3a Jul 10, 2026
f80310a
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into P…
vas3a Jul 10, 2026
160e4f9
PM-5603: Allow active schedules to extend from current end
jmgasper Jul 10, 2026
bfc9564
tags style
vas3a Jul 10, 2026
bb10e32
Merge pull request #2032 from topcoder-platform/PM-5255_showcase-cust…
vas3a Jul 13, 2026
8579532
PM-5255 - showcase media gallery
vas3a Jul 13, 2026
eb1ea6f
PM-5255 - media gallery
vas3a Jul 13, 2026
2fc4c1a
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into P…
vas3a Jul 13, 2026
80a4a1f
lint
vas3a Jul 13, 2026
1f79486
PM-5255 - fetch only PUBLISHED content
vas3a Jul 13, 2026
2c27df8
lint
vas3a Jul 13, 2026
b83b093
Merge pull request #2034 from topcoder-platform/PM-5255_showcase-cust…
vas3a Jul 13, 2026
89ac6e0
PM-5255 - qa fixes
vas3a Jul 13, 2026
29ba7d1
lint
vas3a Jul 13, 2026
c81e1b8
Merge pull request #2035 from topcoder-platform/PM-5255_showcase-cust…
vas3a Jul 13, 2026
ddf4a80
unique skills
vas3a Jul 13, 2026
a6e8d68
Merge pull request #2036 from topcoder-platform/PM-5255_showcase-cust…
vas3a Jul 13, 2026
12e80a8
Mobile Ui
vas3a Jul 13, 2026
f806f46
Merge pull request #2037 from topcoder-platform/PM-5255_showcase-cust…
vas3a Jul 13, 2026
531b85b
Fix for flexi-talent engagements link (PM-5580)
jmgasper Jul 14, 2026
22eef6e
Merge pull request #2033 from topcoder-platform/PM-5603
jmgasper Jul 14, 2026
4147822
Fixes for PM-5585 and PM-5604
jmgasper Jul 14, 2026
c2a49a2
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into dev
jmgasper Jul 14, 2026
c78381f
Lint
jmgasper Jul 14, 2026
e7949dd
Fix for timeline issue noted
jmgasper Jul 14, 2026
53dee70
PM-5255 - customer portal qa fixes
vas3a Jul 14, 2026
18d7aa4
lint
vas3a Jul 14, 2026
130268b
Additional fix for PM-5586
jmgasper Jul 14, 2026
7fef84a
Merge pull request #2038 from topcoder-platform/PM-5255_showcase-cust…
vas3a Jul 14, 2026
80aad9b
save alt for media
vas3a Jul 14, 2026
1345f23
Fix sort, load more, text style
vas3a Jul 14, 2026
3a9c6f7
Merge pull request #2039 from topcoder-platform/PM-5255_showcase-cust…
vas3a Jul 14, 2026
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
1 change: 1 addition & 0 deletions src/apps/customer-portal/src/config/routes.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export const rootRoute: string
: `/${AppSubdomain.customer}`

export const talentSearchRouteId = 'talent-search'
export const showcaseSearchRouteId = 'showcase'
export const flexiTalentRouteId = 'flexi-talent'
2 changes: 2 additions & 0 deletions src/apps/customer-portal/src/customer-portal.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from './config/routes.config'
import { customerPortalFlexiTalentRoutes } from './pages/flexi-talent/flexi-talent.routes'
import { customerPortalTalentSearchRoutes } from './pages/talent-search/talent-search.routes'
import { customerPortalProjectShowcaseRoutes } from './pages/project-showcase/project-showcase.routes'

const CustomerPortalApp: LazyLoadedComponent = lazyLoad(() => import('./CustomerPortalApp'))

Expand All @@ -32,6 +33,7 @@ export const customerPortalRoutes: ReadonlyArray<PlatformRoute> = [
route: '',
},
...customerPortalTalentSearchRoutes,
...customerPortalProjectShowcaseRoutes,
...customerPortalFlexiTalentRoutes,
],
domain: AppSubdomain.customer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import '@libs/ui/styles/includes';

.layout {
position: relative;
// position: relative;
font-family: $font-roboto;
color: var(--Primary);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import _ from 'lodash'
import { TabsNavItem } from '~/libs/ui'
import {
flexiTalentRouteId,
showcaseSearchRouteId,
talentSearchRouteId,
} from '~/apps/customer-portal/src/config/routes.config'

Expand All @@ -12,6 +13,9 @@ export function getTabsConfig(userRoles: string[], isAnonymous: boolean, isUnpri
...(!isUnprivilegedUser ? [{
id: talentSearchRouteId,
title: 'Talent Search',
}, {
id: showcaseSearchRouteId,
title: 'Showcase',
}, {
id: flexiTalentRouteId,
title: 'Flexi-Talent',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
flex-direction: column;
}

:global([class*='ContentLayout-module_content-outer']) {
margin: 0 auto 0 !important;
}

:global([class*='ContentLayout-module_content__']) {
padding-bottom: 0 !important;
}

:global([class*='BreadCrumb-module_breadcrumb']) {
display: none;
}

.blockHeader {
display: flex;
align-items: center;
Expand Down Expand Up @@ -39,3 +51,129 @@
margin-left: $sp-2;
margin-bottom: 6px;
}

.pageArea {
position: relative;
@include substractPagePaddings;
background: $black-5;
}

.pageHero {
width: 100%;
height: 280px;
background-image: url('../../../lib/assets/talent-search-header.png');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}

.pageBody {
display: grid;
grid-template-columns: 443px 1fr;
gap: 24px;
margin-top: -232px;
padding: $sp-2 $sp-8 $sp-10 $sp-8;
position: relative;
z-index: 1;
font-family: $font-roboto;

@include ltemd {
grid-template-columns: 1fr;
margin-top: -232px;
padding: $sp-3 $sp-3 0;
}
}

.sidebar {
display: flex;
flex-direction: column;
gap: 0;

@include ltemd {
margin-left: 0;
position: relative;
z-index: 2;
}
}

.panel {
background: $tc-white;
border: 0;
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
gap: $sp-2;
}

.sidebar .panel + .panel {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 0;
padding-top: 14px;
}

.panelTitle {
color: $black-100;
font-size: 16px;
line-height: 24px;
font-weight: 500;
margin: 0;
}

.resultsPanel {
background: transparent;
border: 0;
border-radius: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
overflow: visible;
min-height: 620px;
}

.resultsPanelEmpty {
background: transparent;
border: 0;
border-radius: 0;
}


.emptyState {
min-height: 470px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: $sp-6;
margin-top: 20vh;
text-align: center;
color: $black-60;

h4 {
margin: 0 0 $sp-2;
color: $black-100;
}

p {
margin: 0 0 $sp-1;
max-width: 500px;
}
}

.emptyStateTitle {
color: $black-100;
font-size: 24px;
line-height: 32px;
font-weight: 700;
margin: 0 0 $sp-2;
}

.emptyStateDescription {
color: $black-100;
font-size: 16px;
line-height: 24px;
font-weight: 400;
margin: 0;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable react/no-array-index-key */
/**
* Page Wrapper.
*/
import { FC, PropsWithChildren, ReactNode } from 'react'
import { FC, PropsWithChildren, ReactNode, useMemo } from 'react'
import { Link } from 'react-router-dom'
import classNames from 'classnames'

Expand All @@ -20,55 +21,91 @@ interface Props {
backUrl?: string
backAction?: () => void
titleUrl?: string | 'emptyLink'
rightHeader?: ReactNode,
rightHeader?: ReactNode
breadCrumb: BreadCrumbData[]
introText: string
shouldShowIntroState?: boolean
sidebar: ReactNode | ReactNode[]
}

export const PageWrapper: FC<PropsWithChildren<Props>> = props => (
<div className={classNames(styles.container, props.className)}>
<BreadCrumb list={props.breadCrumb} />
<PageTitle>{props.pageTitle}</PageTitle>
<div className={styles.blockHeader}>
<div className={styles.blockHeaderRight}>
{props.backUrl && (
<Link to={props.backUrl}>
<IconArrowLeft />
</Link>
)}
{props.backAction && (
<button type='button' onClick={props.backAction}>
<IconArrowLeft />
</button>
)}
<div className={styles.blockHeaderTitle}>
<PageHeader>
<h3 className={styles.textTitle}>
{props.pageTitle}
</h3>
</PageHeader>
{props.titleUrl && props.titleUrl !== 'emptyLink' && (
<a
className={styles.blockExternalLink}
href={props.titleUrl}
target='_blank'
rel='noreferrer'
>
<IconExternalLink />
</a>
export const PageWrapper: FC<PropsWithChildren<Props>> = props => {
const sidebarPanels: ReactNode[] = useMemo(() => (
props.sidebar && !Array.isArray(props.sidebar) ? [props.sidebar] : props.sidebar as ReactNode[]
), [props.sidebar])

return (
<div className={classNames(styles.container, props.className)}>
<BreadCrumb list={props.breadCrumb} />
<PageTitle>{props.pageTitle}</PageTitle>
<div className={styles.blockHeader}>
<div className={styles.blockHeaderRight}>
{props.backUrl && (
<Link to={props.backUrl}>
<IconArrowLeft />
</Link>
)}
{props.titleUrl && props.titleUrl === 'emptyLink' && (
<button type='button' className={styles.blockExternalLink}>
<IconExternalLink />
{props.backAction && (
<button type='button' onClick={props.backAction}>
<IconArrowLeft />
</button>
)}
<div className={styles.blockHeaderTitle}>
<PageHeader>
<h3 className={styles.textTitle}>
{props.pageTitle}
</h3>
</PageHeader>
{props.titleUrl && props.titleUrl !== 'emptyLink' && (
<a
className={styles.blockExternalLink}
href={props.titleUrl}
target='_blank'
rel='noreferrer'
>
<IconExternalLink />
</a>
)}
{props.titleUrl && props.titleUrl === 'emptyLink' && (
<button type='button' className={styles.blockExternalLink}>
<IconExternalLink />
</button>
)}
</div>
</div>

{props.rightHeader}
</div>

{props.rightHeader}
</div>
<div className={styles.pageArea}>
<div className={styles.pageHero} />
<div className={styles.pageBody}>
<aside className={styles.sidebar}>
{sidebarPanels?.map((panel, i) => (
<section className={styles.panel} key={i}>{panel}</section>
))}
</aside>
<section
className={classNames(
styles.resultsPanel,
props.shouldShowIntroState && styles.resultsPanelEmpty,
)}
>
{props.shouldShowIntroState && (
<div className={styles.emptyState}>
<p className={styles.emptyStateDescription}>
{props.introText}
</p>
</div>
)}

{props.children}
</div>
)
{!props.shouldShowIntroState && (
<div className={styles.resultsContent}>{props.children}</div>
)}
</section>
</div>
</div>
</div>
)
}

export default PageWrapper
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ function buildFlexiEngagementWorkLinks(
}

if (normalizedEngagementId) {
links.engagementUrl = `${baseUrl}/projects/${normalizedProjectId}/engagements/${normalizedEngagementId}/view`
links.engagementUrl = `${baseUrl}/projects/${normalizedProjectId}/engagements/${normalizedEngagementId}`
links.assigneeDetailsUrl
= `${baseUrl}/projects/${normalizedProjectId}/engagements/${normalizedEngagementId}/assignments`
}
Expand Down
1 change: 1 addition & 0 deletions src/apps/customer-portal/src/lib/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './talentSearch.service'
export * from './flexiTalent.service'
export * from './showcasePost.service'
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import useSWR, { SWRResponse } from 'swr'

import { Challenge, fetchChallenge } from '~/apps/work/src/lib'

export type UseFetchChallenges = SWRResponse<Challenge[], Error>

export const useFetchChallenges = (challengeIds: string[]): UseFetchChallenges => {
const shouldFetchChallenges = challengeIds.length > 0

return useSWR<Challenge[], Error>(
shouldFetchChallenges ? ['work/challenge-list', challengeIds.join(',')] : undefined,
async () => {
const settledResults = await Promise.allSettled(
challengeIds.map(id => fetchChallenge(id)),
)

const loadedChallenges = settledResults
.filter(
(result): result is PromiseFulfilledResult<Challenge> => result.status === 'fulfilled',
)
.map(result => result.value)

if (loadedChallenges.length === 0 && settledResults.length > 0) {
const rejection = settledResults.find(
(result): result is PromiseRejectedResult => result.status === 'rejected',
)
throw rejection?.reason ?? new Error('Failed to load challenges')
}

return loadedChallenges
},
{
dedupingInterval: 0,
errorRetryCount: 2,
shouldRetryOnError: true,
},
)
}
Loading
Loading