Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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,3 +9,4 @@ export const rootRoute: string
: `/${AppSubdomain.customer}`

export const talentSearchRouteId = 'talent-search'
export const profileCompletionRouteId = 'profile-completion'
6 changes: 4 additions & 2 deletions src/apps/customer-portal/src/customer-portal.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import {
} from '~/libs/core'

import {
profileCompletionRouteId,
rootRoute,
talentSearchRouteId,
} from './config/routes.config'
import { customerPortalTalentSearchRoutes } from './pages/talent-search/talent-search.routes'
import { customerPortalProfileCompletionRoutes } from './pages/profile-completion/profile-completion.routes'

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

Expand All @@ -27,9 +28,10 @@ export const customerPortalRoutes: ReadonlyArray<PlatformRoute> = [
children: [
{
authRequired: true,
element: <Rewrite to={talentSearchRouteId} />,
element: <Rewrite to={profileCompletionRouteId} />,
Comment thread
vas3a marked this conversation as resolved.
route: '',
},
...customerPortalProfileCompletionRoutes,
...customerPortalTalentSearchRoutes,
],
domain: AppSubdomain.customer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import _ from 'lodash'

import { TabsNavItem } from '~/libs/ui'
import {
profileCompletionRouteId,
talentSearchRouteId,
} from '~/apps/customer-portal/src/config/routes.config'

export function getTabsConfig(userRoles: string[], isAnonymous: boolean, isUnprivilegedUser: boolean): TabsNavItem[] {

const tabs: TabsNavItem[] = [
...(!isUnprivilegedUser ? [{
Comment thread
vas3a marked this conversation as resolved.
id: profileCompletionRouteId,
title: 'Profile Completion',
}, {
id: talentSearchRouteId,
title: 'Talent Search',
}] : []),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
@import '@libs/ui/styles/includes';

.container {
display: flex;
flex-direction: column;
gap: $sp-4;
}

.headerRow {
display: flex;
align-items: flex-end;
gap: $sp-4;
justify-content: space-between;

@include ltemd {
flex-direction: column;
align-items: stretch;
}
}

.filterWrap {
min-width: 280px;
max-width: 360px;

@include ltemd {
max-width: unset;
Comment thread
vas3a marked this conversation as resolved.
min-width: unset;
width: 100%;
}
}

.counterCard {
border: 1px solid $black-20;
border-radius: $sp-2;
background: $tc-white;
padding: $sp-4;
min-width: 260px;
display: flex;
flex-direction: column;
gap: $sp-1;
}

.counterLabel {
color: $black-60;
font-size: 12px;
line-height: 16px;
font-weight: 600;
text-transform: uppercase;
}

.counterValue {
color: $black-100;
font-size: 32px;
line-height: 36px;
font-weight: 700;
font-family: 'Nunito Sans', sans-serif;
}

.loadingWrap {
position: relative;
height: 90px;

.spinner {
background: none;
}
}

.errorMessage {
color: $red-100;
font-size: 14px;
line-height: 20px;
font-weight: 700;
}

.emptyMessage {
color: $black-60;
font-size: 14px;
line-height: 20px;
}

.tableWrap {
overflow: auto;
border: 1px solid $black-20;
border-radius: $sp-2;

table {
width: 100%;
border-collapse: collapse;
min-width: 420px;
}

th,
td {
text-align: left;
padding: $sp-3 $sp-4;
border-bottom: 1px solid $black-20;
font-size: 14px;
line-height: 20px;
}

th {
color: $black-100;
font-weight: 700;
background: $black-5;
}

td {
color: $black-100;
}

tr:last-child td {
border-bottom: 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/* eslint-disable react/jsx-no-bind */
Comment thread
vas3a marked this conversation as resolved.
import { ChangeEvent, FC, useMemo, useState } from 'react'
import useSWR, { SWRResponse } from 'swr'

import { EnvironmentConfig } from '~/config'
import { CountryLookup, useCountryLookup, xhrGetAsync } from '~/libs/core'
import { InputSelect, InputSelectOption, LoadingSpinner } from '~/libs/ui'

import { PageWrapper } from '../../../lib'

import styles from './ProfileCompletionPage.module.scss'

type CompletedProfile = {
countryCode?: string
countryName?: string
handle: string
userId?: number | string
}

function normalizeToList(raw: any): any[] {
Comment thread
vas3a marked this conversation as resolved.
if (Array.isArray(raw)) {
return raw
}

if (Array.isArray(raw?.data)) {
return raw.data
}

if (Array.isArray(raw?.result?.content)) {
return raw.result.content
}

if (Array.isArray(raw?.result)) {
return raw.result
}

return []
}

async function fetchCompletedProfiles(): Promise<CompletedProfile[]> {
const response = await xhrGetAsync<CompletedProfile[]>(
`${EnvironmentConfig.REPORTS_API}/topcoder/completed-profiles`,
)

return normalizeToList(response)
}

export const ProfileCompletionPage: FC = () => {
const [selectedCountry, setSelectedCountry] = useState<string>('all')
const countryLookup: CountryLookup[] | undefined = useCountryLookup()

const { data, error, isValidating }: SWRResponse<CompletedProfile[]> = useSWR(
'customer-portal-completed-profiles',
fetchCompletedProfiles,
{
revalidateOnFocus: false,
},
)

const countryMap = useMemo(() => {
const map = new Map<string, string>();
(countryLookup || []).forEach(country => {
if (country.countryCode) {
map.set(country.countryCode, country.country)
}
})

return map
}, [countryLookup])

const countryOptions = useMemo<InputSelectOption[]>(() => {
const dynamicCodes = new Set<string>();
(data || []).forEach(profile => {
if (profile.countryCode) {
dynamicCodes.add(profile.countryCode)
}
})

const dynamicOptions = Array.from(dynamicCodes)
.map(code => ({
label: countryMap.get(code) || code,
value: code,
}))
.sort((a, b) => String(a.label)
.localeCompare(String(b.label)))

return [
{
label: 'All Countries',
value: 'all',
},
...dynamicOptions,
]
}, [countryMap, data])

const profiles = useMemo(() => {
const source = data || []
if (selectedCountry === 'all') {
return source
}

return source.filter(profile => profile.countryCode === selectedCountry)
}, [data, selectedCountry])

const displayedRows = useMemo(() => profiles
.map(profile => ({
...profile,
countryLabel: profile.countryCode
? countryMap.get(profile.countryCode) || profile.countryName || profile.countryCode
: profile.countryName || '-',
}))
.sort((a, b) => a.handle.localeCompare(b.handle)), [profiles, countryMap])

return (
<PageWrapper
pageTitle='Profile Completion'
breadCrumb={[]}
className={styles.container}
>
<div className={styles.headerRow}>
<div className={styles.filterWrap}>
<InputSelect
name='country'
label='Country'
options={countryOptions}
value={selectedCountry}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setSelectedCountry(event.target.value || 'all')
Comment thread
vas3a marked this conversation as resolved.
}}
placeholder='Select country'
/>
</div>
<div className={styles.counterCard}>
<span className={styles.counterLabel}>Fully Completed Profiles</span>
<strong className={styles.counterValue}>{profiles.length}</strong>
</div>
</div>

{isValidating && !data && (
<div className={styles.loadingWrap}>
<LoadingSpinner className={styles.spinner} />
</div>
)}

{!isValidating && !!error && (
<div className={styles.errorMessage}>
Failed to load profile completion data.
</div>
)}

{!error && !isValidating && displayedRows.length === 0 && (
<div className={styles.emptyMessage}>
No fully completed profiles found for the selected country.
</div>
)}

{!error && displayedRows.length > 0 && (
<div className={styles.tableWrap}>
<table>
<thead>
<tr>
<th>Handle</th>
<th>Country</th>
</tr>
</thead>
<tbody>
{displayedRows.map(profile => (
<tr key={profile.userId || profile.handle}>
Comment thread
vas3a marked this conversation as resolved.
<td>
<a
href={`${EnvironmentConfig.USER_PROFILE_URL}/${profile.handle}`}
target='_blank'
rel='noreferrer noopener'
>
{profile.handle}
</a>
</td>
<td>{profile.countryLabel}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</PageWrapper>
)
}

export default ProfileCompletionPage
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ProfileCompletionPage } from './ProfileCompletionPage'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ProfileCompletionPage'
Comment thread
vas3a marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core'

import { profileCompletionRouteId } from '../../config/routes.config'

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

export const profileCompletionChildRoutes = [
{
authRequired: true,
element: <ProfileCompletionPage />,
id: 'profile-completion-page',
route: '',
Comment thread
vas3a marked this conversation as resolved.
},
]

export const customerPortalProfileCompletionRoutes = [
{
children: [...profileCompletionChildRoutes],
element: getRoutesContainer(profileCompletionChildRoutes),
id: profileCompletionRouteId,
route: profileCompletionRouteId,
},
]
1 change: 1 addition & 0 deletions src/config/environments/default.env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const API = {
export const STANDARDIZED_SKILLS_API = `${API.V5}/standardized-skills`
export const TC_FINANCE_API = `${API.V6}/finance`
export const TC_AI_API = `${API.V6}/ai`
export const REPORTS_API = `${API.V6}/reports`

export const AUTH = {
ACCOUNTS_APP_CONNECTOR: `https://accounts-auth0.${TC_DOMAIN}`,
Expand Down
1 change: 1 addition & 0 deletions src/config/environments/global-config.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface GlobalConfig {
STANDARDIZED_SKILLS_API: string,
TC_FINANCE_API: string,
TC_AI_API: string,
REPORTS_API: string,
AUTH: {
ACCOUNTS_APP_CONNECTOR: string
}
Expand Down
Loading
Loading